diff --git a/.speakeasy/workflow.lock b/.speakeasy/workflow.lock index ea412a4a7..0ab14de46 100644 --- a/.speakeasy/workflow.lock +++ b/.speakeasy/workflow.lock @@ -1,9 +1,9 @@ -speakeasyVersion: 1.358.0 +speakeasyVersion: 1.366.0 sources: merge-code-samples-into-spec: sourceNamespace: merge-code-samples-into-spec - sourceRevisionDigest: sha256:43c01a4167a6e338d86fe9b644dbf8d8aabba8204a7cfb69a4d880a43f75d33f - sourceBlobDigest: sha256:5a2f142dc7d22ff664601f4f240c011b60a7f1de4f1b93f8b056114aba90b386 + sourceRevisionDigest: sha256:61d763ced26b2104e5f13eb6e4b32bd0f598073da2505d842baaa9efd4c78ff1 + sourceBlobDigest: sha256:d53140203694ab341c7f9b60bcfce1995e2c287358ec29504fcb45b677d38918 tags: - latest - main diff --git a/apps/magic-link/src/components/Modal.tsx b/apps/magic-link/src/components/Modal.tsx index 1aa18f7af..095e5a539 100644 --- a/apps/magic-link/src/components/Modal.tsx +++ b/apps/magic-link/src/components/Modal.tsx @@ -2,29 +2,39 @@ import React, { useState } from 'react' import {X} from 'lucide-react' -const Modal = ({open,setOpen,children} : {open:boolean,setOpen: (op : boolean) => void,children: React.ReactNode}) => { + +interface ModalProps { + open: boolean; + setOpen: (op: boolean) => void; + children: React.ReactNode; + backgroundClass?: string; + contentClass?: string; +} + +const Modal: React.FC = ({ + open, + setOpen, + children, + backgroundClass = "bg-black/20 backdrop-blur ", + contentClass = "" +}) => { + if (!open) return null; + return (
setOpen(false)} className={` fixed inset-0 flex justify-center items-center transition-colors - ${open ? "visible bg-black/20 backdrop-blur" : "invisible"} + ${backgroundClass} `} > {/* modal */}
e.stopPropagation()} className={` - bg-[#1d1d1d] border-green-900 rounded-xl shadow p-6 transition-all - ${open ? "scale-100 opacity-100" : "scale-125 opacity-0"} + ${contentClass} transition-all `} > - {children}
diff --git a/apps/magic-link/src/index.css b/apps/magic-link/src/index.css index 5161271e3..3d400a5fd 100644 --- a/apps/magic-link/src/index.css +++ b/apps/magic-link/src/index.css @@ -53,7 +53,4 @@ * { @apply border-border; } - body { - @apply bg-background text-foreground; - } } diff --git a/apps/magic-link/src/lib/ProviderModal.tsx b/apps/magic-link/src/lib/ProviderModal.tsx index e77289c77..e08a2238c 100644 --- a/apps/magic-link/src/lib/ProviderModal.tsx +++ b/apps/magic-link/src/lib/ProviderModal.tsx @@ -1,32 +1,21 @@ -import { useEffect, useState } from 'react'; -import useOAuth from '@/hooks/useOAuth'; -import { providersArray, categoryFromSlug, Provider,CONNECTORS_METADATA,AuthStrategy } from '@panora/shared/src'; -import { categoriesVerticals } from '@panora/shared/src/categories'; -import useUniqueMagicLink from '@/hooks/queries/useUniqueMagicLink'; -import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; -import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; -import { Label } from '@/components/ui/label'; -import { Button } from '@/components/ui/button'; -import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { LoadingSpinner } from '@/components/ui/loading-spinner'; -import useProjectConnectors from '@/hooks/queries/useProjectConnectors'; -import {ArrowLeftRight} from 'lucide-react'; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import * as z from "zod"; -import { Input } from "@/components/ui/input"; import Modal from '@/components/Modal'; +import { Button } from '@/components/ui/button'; import { Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" + DialogContent +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { LoadingSpinner } from '@/components/ui/loading-spinner'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import useCreateApiKeyConnection from '@/hooks/queries/useCreateApiKeyConnection'; - +import useProjectConnectors from '@/hooks/queries/useProjectConnectors'; +import useUniqueMagicLink from '@/hooks/queries/useUniqueMagicLink'; +import useOAuth from '@/hooks/useOAuth'; +import { AuthStrategy, categoryFromSlug, CONNECTORS_METADATA, Provider, providersArray } from '@panora/shared/src'; +import { categoriesVerticals } from '@panora/shared/src/categories'; +import { ArrowLeft, ArrowLeftRight, Search, X } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { useForm } from "react-hook-form"; interface IBasicAuthFormData { [key : string]: string @@ -73,6 +62,7 @@ const ProviderModal = () => { const {data: magicLink} = useUniqueMagicLink(uniqueMagicLinkId); const {data: connectorsForProject} = useProjectConnectors(isProjectIdReady ? projectId : null); const {register: register2, formState: {errors: errors2}, handleSubmit: handleSubmit2, reset: reset2} = useForm(); + const [searchTerm, setSearchTerm] = useState(''); useEffect(() => { const queryParams = new URLSearchParams(window.location.search); @@ -286,165 +276,272 @@ const ProviderModal = () => { setStartFlow(true); } + const filteredProviders = data.filter(provider => + provider.name.toLowerCase().includes(searchTerm.toLowerCase()) && + (selectedCategory === "All" || provider.vertical?.toLowerCase() === selectedCategory.toLowerCase()) + ); + + const handleProviderSelect = (provider: Provider) => { + setSelectedProvider({ provider: provider.name.toLowerCase(), category: provider.vertical!.toLowerCase() }); + const logoPath = CONNECTORS_METADATA[provider.vertical!.toLowerCase()][provider.name.toLowerCase()].logoPath; + setCurrentProviderLogoURL(logoPath); + setCurrentProvider(provider.name.toLowerCase()) + + const providerMetadata = CONNECTORS_METADATA[provider.vertical!.toLowerCase()][provider.name.toLowerCase()]; + if (providerMetadata.authStrategy.strategy === AuthStrategy.api_key || providerMetadata.authStrategy.strategy === AuthStrategy.basic) { + setOpenBasicAuthDialog(true); + } else if (providerMetadata?.options?.end_user_domain) { + setOpenDomainDialog(true); + } else { + setLoading({ status: true, provider: provider.name.toLowerCase() }); + setStartFlow(true); + } + }; + + const formatProvider = (provider: string) => { + return provider.substring(0,1).toUpperCase() + provider.substring(1) + } + const formatVertical = (vertical: string) => { + switch(vertical){ + case "marketingautomation": + return 'Marketing Automation'; + case "filestorage": + return "File Storage"; + case "crm": + return "CRM"; + case "ats": + return "ATS"; + case "hris": + return "HRIS"; + default: + return vertical.substring(0,1).toUpperCase() + vertical.substring(1) + } + } return ( - <> - - - Connect to your software - setSearchTerm(e.target.value)} + className="pl-10" + /> + + + - - -
- - {(data as Provider[]).map((provider) => ( -
- handleWalletClick(provider.name, provider.vertical!)} - /> - + + +
+ +
+ {filteredProviders.map((provider) => ( +
handleProviderSelect(provider)} + > +
+ {provider.name} +
+ {formatProvider(provider.name)}
))} -
- - +
+ + {/* Basic Auth Dialog */} + + +
+ +
+ +
- {loading.status ? : } - {errorResponse.errorPresent ?

{errorResponse.errorMessage}

: (<>)} - - {/*
*/} - -
- - {/* Dialog for basic auth input */} - - - - Enter your credentials - {domainFormats[selectedProvider?.provider.toLowerCase()] && ( - (e.g., {domainFormats[selectedProvider?.provider.toLowerCase()]}) - )} - - {/*
*/} - -
-
- {selectedProvider.provider!=='' && selectedProvider.category!=='' && CONNECTORS_METADATA[selectedProvider.category][selectedProvider.provider].authStrategy.properties?.map((fieldName : string) => - ( - <> - - +
+ {selectedProvider.provider} +
+

+ Connect your {selectedProvider.provider.charAt(0).toUpperCase() + selectedProvider.provider.slice(1)} Account +

+ + )} + + + {selectedProvider.provider !== '' && selectedProvider.category !== '' && + CONNECTORS_METADATA[selectedProvider.category][selectedProvider.provider].authStrategy.properties?.map((fieldName: string) => ( +
+ - {errors2[fieldName] && (

{errors2[fieldName]?.message}

)} - - ))} + /> + {errors2[fieldName] &&

{errors2[fieldName]?.message}

} +
+ ))} + +

+ A third-party accountant will be added. +

+ + +
-
- - - - - - {/* */} -
-
- - {/* Dialog for end-user domain input */} - - - - Enter Your Domain - -
{ e.preventDefault(); onDomainSubmit(); }}> -
-
- + +
+ + {/* Domain Dialog */} + + +
+ +
+ +
+ {selectedProvider?.category && selectedProvider?.provider && CONNECTORS_METADATA[selectedProvider.category]?.[selectedProvider.provider] && ( + <> +
+ {selectedProvider.provider} +
+

+ Connect your {selectedProvider.provider.charAt(0).toUpperCase() + selectedProvider.provider.slice(1)} Account +

+ + )} + + { e.preventDefault(); onDomainSubmit(); }} className="w-full space-y-4"> +
setEndUserDomain(e.target.value)} /> -
{errors2.end_user_domain && (

{errors2.end_user_domain.message}

)}
+ {errors2.end_user_domain &&

{errors2.end_user_domain.message}

}
-
- - - - - + +
-
- - {/* OAuth Successful Modal */} - -
-
-
- - - - {selectedProvider?.provider} - + + + {/* Success Dialog */} + +
+
+
+ +
+
+ + +
+ {currentProvider} +
- -
Connection Successful!
- -
The connection with {currentProvider} was successfully established. You can visit the Dashboard and verify the status.
- +

+ Your data is being imported... +

+
+ + You've successfully connected your account!
+ +
+
+
+ + {/* Loading state */} + {loading.status && ( +
+
+ + Connecting to {loading.provider}... +
+
+ )} + + {/* Error message */} + {errorResponse.errorPresent && ( +
+

{errorResponse.errorMessage}

+
+ )}
- - ); }; diff --git a/apps/webapp/src/components/Configuration/Connector/ConnectorDisplay.tsx b/apps/webapp/src/components/Configuration/Connector/ConnectorDisplay.tsx index 16c2068e8..94a38e8c5 100644 --- a/apps/webapp/src/components/Configuration/Connector/ConnectorDisplay.tsx +++ b/apps/webapp/src/components/Configuration/Connector/ConnectorDisplay.tsx @@ -1,99 +1,81 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { useForm } from "react-hook-form" -import { Button } from "@/components/ui/button" -import { Label } from "@/components/ui/label" -import { Separator } from "@/components/ui/separator" -import { Switch } from "@/components/ui/switch" -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form" -import { PasswordInput } from "@/components/ui/password-input" -import { z } from "zod" -import config from "@/lib/config" -import { AuthStrategy, providerToType, Provider,CONNECTORS_METADATA, extractProvider, extractVertical, needsSubdomain, needsScope } from "@panora/shared" -import { useEffect, useState } from "react" -import useProjectStore from "@/state/projectStore" -import { usePostHog } from 'posthog-js/react' -import { Input } from "@/components/ui/input" -import useConnectionStrategies from "@/hooks/get/useConnectionStrategies" -import { DataTableFacetedFilter } from "@/components/shared/data-table-faceted-filter" -import useCreateConnectionStrategy from "@/hooks/create/useCreateConnectionStrategy" -import useUpdateConnectionStrategy from "@/hooks/update/useUpdateConnectionStrategy" -import useConnectionStrategyAuthCredentials from "@/hooks/get/useConnectionStrategyAuthCredentials" -import { useQueryClient } from "@tanstack/react-query" -import { toast } from "sonner" +import React, { useEffect, useState } from 'react'; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQueryClient } from "@tanstack/react-query"; +import { usePostHog } from 'posthog-js/react'; +import { toast } from "sonner"; +import * as z from "zod"; -interface ItemDisplayProps { - item?: Provider -} +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; +import { Switch } from "@/components/ui/switch"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { PasswordInput } from "@/components/ui/password-input"; +import { Input } from "@/components/ui/input"; +import { DataTableFacetedFilter } from "@/components/shared/data-table-faceted-filter"; + +import config from "@/lib/config"; +import { AuthStrategy, providerToType, Provider, CONNECTORS_METADATA, extractProvider, extractVertical, needsSubdomain, needsScope } from "@panora/shared"; +import useProjectStore from "@/state/projectStore"; +import useConnectionStrategies from "@/hooks/get/useConnectionStrategies"; +import useCreateConnectionStrategy from "@/hooks/create/useCreateConnectionStrategy"; +import useUpdateConnectionStrategy from "@/hooks/update/useUpdateConnectionStrategy"; +import useConnectionStrategyAuthCredentials from "@/hooks/get/useConnectionStrategyAuthCredentials"; const formSchema = z.object({ - subdomain: z.string({ - required_error: "Please Enter a Subdomain", - }).optional(), - client_id : z.string({ - required_error: "Please Enter a Client ID", - }).optional(), - client_secret : z.string({ - required_error: "Please Enter a Client Secret", - }).optional(), - scope : z.string({ - required_error: "Please Enter a scope", - }).optional(), - api_key: z.string({ - required_error: "Please Enter a API Key", - }).optional(), - username: z.string({ - required_error: "Please Enter Username", - }).optional(), - secret: z.string({ - required_error: "Please Enter Secret", - }).optional(), -}) + subdomain: z.string().optional(), + client_id: z.string().optional(), + client_secret: z.string().optional(), + scope: z.string().optional(), + api_key: z.string().optional(), + username: z.string().optional(), + secret: z.string().optional(), +}); + +interface ItemDisplayProps { + item?: Provider; +} export function ConnectorDisplay({ item }: ItemDisplayProps) { const [copied, setCopied] = useState(false); const [switchEnabled, setSwitchEnabled] = useState(false); - const { idProject } = useProjectStore() - const { data: connectionStrategies, isLoading: isConnectionStrategiesLoading, error: isConnectionStategiesError } = useConnectionStrategies() - const { createCsPromise } = useCreateConnectionStrategy(); - const { updateCsPromise } = useUpdateConnectionStrategy() - const { mutateAsync: fetchCredentials, data: fetchedData } = useConnectionStrategyAuthCredentials(); + const { idProject } = useProjectStore(); const queryClient = useQueryClient(); + const posthog = usePostHog(); - const posthog = usePostHog() + const { data: connectionStrategies } = useConnectionStrategies(); + const { createCsPromise } = useCreateConnectionStrategy(); + const { updateCsPromise } = useUpdateConnectionStrategy(); + const { mutateAsync: fetchCredentials } = useConnectionStrategyAuthCredentials(); - const mappingConnectionStrategies = connectionStrategies?.filter((cs) => extractVertical(cs.type).toLowerCase() == item?.vertical && extractProvider(cs.type).toLowerCase() == item?.name) const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { - subdomain: "", - client_id: "", - client_secret: "", - scope: "", - api_key: "", - username: "", - secret: "", + subdomain: "", client_id: "", client_secret: "", scope: "", + api_key: "", username: "", secret: "", }, - }) + }); - // Extract oauth_attributes from the connector metadata - const oauthAttributes = CONNECTORS_METADATA[item?.vertical!][item?.name!].options?.oauth_attributes || []; + const mappingConnectionStrategies = connectionStrategies?.filter( + (cs) => extractVertical(cs.type).toLowerCase() === item?.vertical && + extractProvider(cs.type).toLowerCase() === item?.name + ); - // Update the form schema to include dynamic fields - oauthAttributes.forEach((attr: string) => { - formSchema.shape[attr as keyof z.infer] = z.string().optional(); // Add each attribute as an optional string - }); + const oauthAttributes = CONNECTORS_METADATA[item?.vertical!]?.[item?.name!]?.options?.oauth_attributes || []; + + useEffect(() => { + oauthAttributes.forEach((attr: string) => { + formSchema.shape[attr as keyof z.infer] = z.string().optional(); + }); + }, [oauthAttributes]); const handleCopy = async () => { try { - await navigator.clipboard.writeText(`${config.API_URL}/connections/oauth/callback`) + await navigator.clipboard.writeText(`${config.API_URL}/connections/oauth/callback`); setCopied(true); - toast.success("Redirect uri copied", { - action: { - label: "Close", - onClick: () => console.log("Close"), - }, - }) - setTimeout(() => setCopied(false), 2000); // Reset copied state after 2 seconds + toast.success("Redirect URI copied"); + setTimeout(() => setCopied(false), 2000); } catch (err) { console.error('Failed to copy: ', err); } @@ -102,287 +84,128 @@ export function ConnectorDisplay({ item }: ItemDisplayProps) { function onSubmit(values: z.infer) { const { client_id, client_secret, scope, api_key, secret, username, subdomain } = values; const performUpdate = mappingConnectionStrategies && mappingConnectionStrategies.length > 0; - const dynamicAttributes = oauthAttributes.map((attr: string) => values[attr as keyof z.infer]).filter((value: any) => value !== undefined); + const dynamicAttributes = oauthAttributes + .map((attr: string) => values[attr as keyof z.infer]) + .filter((value): value is string => value !== undefined); + + let attributes: string[] = []; + let attributeValues: string[] = []; + switch (item?.authStrategy.strategy) { case AuthStrategy.oauth2: - const needs_subdomain = needsSubdomain(item.name.toLowerCase(), item.vertical!.toLowerCase()); - const needs_scope = needsScope(item.name.toLowerCase(), item.vertical!.toLowerCase()); - if (client_id === "" || client_secret === "") { - if (client_id === "") { - form.setError("client_id", { "message": "Please Enter Client ID" }); - } - if (client_secret === "") { - form.setError("client_secret", { "message": "Please Enter Client Secret" }); - } - if (scope === "") { - form.setError("scope", { "message": "Please Enter the scope" }); - } - break; - } - if(needs_subdomain && subdomain == ""){ - form.setError("subdomain", { "message": "Please Enter Subdomain" }); + if (!client_id || !client_secret) { + form.setError("client_id", { message: "Please Enter Client ID" }); + form.setError("client_secret", { message: "Please Enter Client Secret" }); + return; } - if (needs_scope && scope === "") { - form.setError("scope", { "message": "Please Enter the scope" }); - } - let ATTRIBUTES = ["client_id", "client_secret", ...oauthAttributes]; - let VALUES = [client_id!, client_secret!, ...dynamicAttributes]; - if(needs_subdomain){ - ATTRIBUTES.push("subdomain") - VALUES.push(subdomain!) - } - if(needs_scope){ - ATTRIBUTES.push("scope") - VALUES.push(scope!) + attributes = ["client_id", "client_secret", ...oauthAttributes]; + attributeValues = [client_id, client_secret, ...dynamicAttributes]; + + if (needsSubdomain(item.name.toLowerCase(), item.vertical!.toLowerCase())) { + if (!subdomain) { + form.setError("subdomain", { message: "Please Enter Subdomain" }); + return; + } + attributes.push("subdomain"); + attributeValues.push(subdomain); } - if (performUpdate) { - const dataToUpdate = mappingConnectionStrategies[0]; - toast.promise( - updateCsPromise({ - id_cs: dataToUpdate.id_connection_strategy, - updateToggle: false, - status: dataToUpdate.status, - attributes: ATTRIBUTES, - values: VALUES as string[] - }), - { - loading: 'Loading...', - success: (data: any) => { - queryClient.setQueryData(['connection-strategies'], (oldQueryData = []) => { - return oldQueryData.map((CS) => CS.id_connection_strategy === data.id_connection_strategy ? data : CS) - }); - return ( -
- -
- Changes have been saved -
-
- ) - ; - }, - error: (err: any) => err.message || 'Error' - }); - posthog?.capture("Connection_strategy_OAuth2_updated", { - id_project: idProject, - mode: config.DISTRIBUTION - }); - } else { - toast.promise( - createCsPromise({ - type: providerToType(item?.name, item?.vertical!, AuthStrategy.oauth2), - attributes: ATTRIBUTES, - values: VALUES as string[] - }), - { - loading: 'Loading...', - success: (data: any) => { - queryClient.setQueryData(['connections-strategies'], (oldQueryData = []) => { - return [...oldQueryData, data]; - }); - return ( -
- -
- Changes have been saved -
-
- ) - ; - }, - error: (err: any) => err.message || 'Error' - }); - posthog?.capture("Connection_strategy_OAuth2_created", { - id_project: idProject, - mode: config.DISTRIBUTION - }); + + if (needsScope(item.name.toLowerCase(), item.vertical!.toLowerCase())) { + if (!scope) { + form.setError("scope", { message: "Please Enter the scope" }); + return; + } + attributes.push("scope"); + attributeValues.push(scope); } - form.reset(); break; case AuthStrategy.api_key: - if (values.api_key === "") { - form.setError("api_key", { "message": "Please Enter API Key" }); - break; - } - if (performUpdate) { - const dataToUpdate = mappingConnectionStrategies[0]; - toast.promise( - updateCsPromise({ - id_cs: dataToUpdate.id_connection_strategy, - updateToggle: false, - status: dataToUpdate.status, - attributes: ["api_key"], - values: [api_key!] - }), - { - loading: 'Loading...', - success: (data: any) => { - queryClient.setQueryData(['connection-strategies'], (oldQueryData = []) => { - return oldQueryData.map((CS) => CS.id_connection_strategy === data.id_connection_strategy ? data : CS) - }); - return ( -
- -
- Changes have been saved -
-
- ) - ; - }, - error: (err: any) => err.message || 'Error' - }); - posthog?.capture("Connection_strategy_API_KEY_updated", { - id_project: idProject, - mode: config.DISTRIBUTION - }); - } else { - toast.promise( - createCsPromise({ - type: providerToType(item?.name, item?.vertical!, AuthStrategy.api_key), - attributes: ["api_key"], - values: [api_key!] - }), - { - loading: 'Loading...', - success: (data: any) => { - queryClient.setQueryData(['connections-strategies'], (oldQueryData = []) => { - return [...oldQueryData, data]; - }); - return ( -
- -
- Changes have been saved -
-
- ) - ; - }, - error: (err: any) => err.message || 'Error' - }); - posthog?.capture("Connection_strategy_API_KEY_created", { - id_project: idProject, - mode: config.DISTRIBUTION - }); + if (!api_key) { + form.setError("api_key", { message: "Please Enter API Key" }); + return; } - form.reset(); + attributes = ["api_key"]; + attributeValues = [api_key]; break; case AuthStrategy.basic: - if (values.username === "" || values.secret === "") { - if (values.username === "") { - form.setError("username", { "message": "Please Enter Username" }); - } - if (values.secret === "") { - form.setError("secret", { "message": "Please Enter Secret" }); - } - break; + if (!username || !secret) { + form.setError("username", { message: "Please Enter Username" }); + form.setError("secret", { message: "Please Enter Secret" }); + return; } - if (performUpdate) { - const dataToUpdate = mappingConnectionStrategies[0]; - toast.promise( - updateCsPromise({ - id_cs: dataToUpdate.id_connection_strategy, - updateToggle: false, - status: dataToUpdate.status, - attributes: ["username", "secret"], - values: [username!, secret!] - }), - { - loading: 'Loading...', - success: (data: any) => { - queryClient.setQueryData(['connection-strategies'], (oldQueryData = []) => { - return oldQueryData.map((CS) => CS.id_connection_strategy === data.id_connection_strategy ? data : CS) - }); - return ( -
- -
- Changes have been saved -
-
- ) - ; - }, - error: (err: any) => err.message || 'Error' - }); - posthog?.capture("Connection_strategy_BASIC_AUTH_updated", { - id_project: idProject, - mode: config.DISTRIBUTION - }); - - } else { - toast.promise( - createCsPromise({ - type: providerToType(item?.name, item?.vertical!, AuthStrategy.basic), - attributes: ["username", "secret"], - values: [username!, secret!] - }), - { - loading: 'Loading...', - success: (data: any) => { - queryClient.setQueryData(['connections-strategies'], (oldQueryData = []) => { - return [...oldQueryData, data]; - }); - return ( -
- -
- Changes have been saved -
-
- ) - ; - }, - error: (err: any) => err.message || 'Error' - }); - posthog?.capture("Connection_strategy_BASIC_AUTH_created", { - id_project: idProject, - mode: config.DISTRIBUTION - }); - } - form.reset(); + attributes = ["username", "secret"]; + attributeValues = [username, secret]; break; } + + const promise = performUpdate + ? updateCsPromise({ + id_cs: mappingConnectionStrategies[0].id_connection_strategy, + updateToggle: false, + status: mappingConnectionStrategies[0].status, + attributes, + values: attributeValues, + }) + : createCsPromise({ + type: providerToType(item?.name!, item?.vertical!, item?.authStrategy.strategy!), + attributes, + values: attributeValues, + }); + + toast.promise(promise, { + loading: 'Saving changes...', + success: (data: any) => { + queryClient.setQueryData( + ['connection-strategies'], + (oldData = []) => performUpdate + ? oldData.map(cs => cs.id_connection_strategy === data.id_connection_strategy ? data : cs) + : [...oldData, data] + ); + return "Changes saved successfully"; + }, + error: (err: any) => err.message || 'An error occurred', + }); + + posthog?.capture(`Connection_strategy_${item?.authStrategy.strategy}_${performUpdate ? 'updated' : 'created'}`, { + id_project: idProject, + mode: config.DISTRIBUTION + }); + + form.reset(); } useEffect(() => { if (mappingConnectionStrategies && mappingConnectionStrategies.length > 0) { - fetchCredentials({ - type: mappingConnectionStrategies[0].type, - attributes: item?.authStrategy.strategy === AuthStrategy.oauth2 ? needsSubdomain(item.name.toLowerCase(), item.vertical!.toLowerCase()) ? ["subdomain", "client_id", "client_secret", ...oauthAttributes] : needsScope(item.name.toLowerCase(), item.vertical!.toLowerCase()) ? ["client_id", "client_secret","scope", ...oauthAttributes] : ["client_id", "client_secret", ...oauthAttributes] - : item?.authStrategy.strategy === AuthStrategy.api_key ? ["api_key"] : ["username", "secret"] - }, { + const attributes = + item?.authStrategy.strategy === AuthStrategy.oauth2 + ? [...(needsSubdomain(item.name.toLowerCase(), item.vertical!.toLowerCase()) ? ["subdomain"] : []), + "client_id", "client_secret", + ...(needsScope(item.name.toLowerCase(), item.vertical!.toLowerCase()) ? ["scope"] : []), + ...oauthAttributes] + : item?.authStrategy.strategy === AuthStrategy.api_key + ? ["api_key"] + : ["username", "secret"]; + + fetchCredentials({ type: mappingConnectionStrategies[0].type, attributes }, { onSuccess(data) { + let i = 0; if (item?.authStrategy.strategy === AuthStrategy.oauth2) { - let i = 0; if (needsSubdomain(item.name.toLowerCase(), item.vertical?.toLowerCase()!)) { - form.setValue("subdomain", data[i]); - i += 1; + form.setValue("subdomain", data[i++]); } - - // Set client_id and client_secret - form.setValue("client_id", data[i]); - form.setValue("client_secret", data[i + 1]); - i += 2; // Increment i after setting client_id and client_secret - - // Check if scope is needed and set the value if so + form.setValue("client_id", data[i++]); + form.setValue("client_secret", data[i++]); if (needsScope(item.name.toLowerCase(), item.vertical?.toLowerCase()!)) { - form.setValue("scope", data[i]); - i += 1; + form.setValue("scope", data[i++]); } - - // Set any additional OAuth attributes - oauthAttributes.forEach((attr: string, index: number) => { - form.setValue(attr as keyof z.infer, data[i + index]); + oauthAttributes.forEach((attr: string) => { + form.setValue(attr as keyof z.infer, data[i++]); }); - } - if (item?.authStrategy.strategy === AuthStrategy.api_key) { + } else if (item?.authStrategy.strategy === AuthStrategy.api_key) { form.setValue("api_key", data[0]); - } - if (item?.authStrategy.strategy === AuthStrategy.basic) { + } else if (item?.authStrategy.strategy === AuthStrategy.basic) { form.setValue("username", data[0]); form.setValue("secret", data[1]); } @@ -402,47 +225,36 @@ export function ConnectorDisplay({ item }: ItemDisplayProps) { updateCsPromise({ id_cs: dataToUpdate.id_connection_strategy, updateToggle: true - }), - { - loading: 'Loading...', + }), + { + loading: 'Updating status...', success: (data: any) => { - queryClient.setQueryData(['connection-strategies'], (oldQueryData = []) => { - return oldQueryData.map((CS) => CS.id_connection_strategy === data.id_connection_strategy ? data : CS) - }); - return ( -
- -
- Changes have been saved -
-
- ) - ; + queryClient.setQueryData(['connection-strategies'], (oldData = []) => + oldData.map(cs => cs.id_connection_strategy === data.id_connection_strategy ? data : cs) + ); + return "Status updated successfully"; }, - error: (err: any) => err.message || 'Error' - }); - + error: (err: any) => err.message || 'An error occurred', + } + ); setSwitchEnabled(enabled); } }; return ( -
+
{item ? (
- + {item.name}
{`${item.name.substring(0, 1).toUpperCase()}${item.name.substring(1)}`}
{item.description}
{mappingConnectionStrategies && mappingConnectionStrategies.length > 0 && (
-
@@ -623,5 +420,5 @@ export function ConnectorDisplay({ item }: ItemDisplayProps) {
)}
- ) -} + ); +} \ No newline at end of file diff --git a/apps/webapp/src/components/Nav/main-nav.tsx b/apps/webapp/src/components/Nav/main-nav.tsx index 1efe65cf5..7732ae037 100644 --- a/apps/webapp/src/components/Nav/main-nav.tsx +++ b/apps/webapp/src/components/Nav/main-nav.tsx @@ -82,13 +82,11 @@ export function MainNav({ ); } -const navIconClassName = "text-gray-400 w-5"; const navItems: Omit[] = [ { name: 'connections', content: ( <> - Connections ), @@ -97,7 +95,6 @@ const navItems: Omit[] = [ name: 'events', content: ( <> - Events ), @@ -106,7 +103,6 @@ const navItems: Omit[] = [ name: 'configuration', content: ( <> - Configuration ), @@ -115,7 +111,6 @@ const navItems: Omit[] = [ name: 'api-keys', content: ( <> - API Keys ), @@ -124,7 +119,6 @@ const navItems: Omit[] = [ name: 'docs', content: ( <> -

Docs

), diff --git a/apps/webapp/src/components/shared/team-switcher.tsx b/apps/webapp/src/components/shared/team-switcher.tsx index 5121ad8a7..4a0328eda 100644 --- a/apps/webapp/src/components/shared/team-switcher.tsx +++ b/apps/webapp/src/components/shared/team-switcher.tsx @@ -70,7 +70,7 @@ const projectFormSchema = z.object({ type PopoverTriggerProps = React.ComponentPropsWithoutRef interface TeamSwitcherProps extends PopoverTriggerProps { - projects:Project[] + projects: Project[] } interface ModalObj { @@ -88,7 +88,7 @@ export default function TeamSwitcher({ className ,projects}: TeamSwitcherProps) const { profile } = useProfileStore(); const { idProject, setIdProject } = useProjectStore(); - const {mutate : refreshAccessToken} = useRefreshAccessTokenMutation() + const { mutate : refreshAccessToken } = useRefreshAccessTokenMutation() const handleOpenChange = (open: boolean) => { setShowNewDialog(prevState => ({ ...prevState, open })); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 1454bb7c4..061735d37 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -150,10 +150,10 @@ services: FACTORIAL_HRIS_CLOUD_CLIENT_SECRET: ${FACTORIAL_ATS_CLOUD_CLIENT_SECRET} PAYFIT_HRIS_CLOUD_CLIENT_ID: ${PAYFIT_HRIS_CLOUD_CLIENT_ID} PAYFIT_HRIS_CLOUD_CLIENT_SECRET: ${PAYFIT_HRIS_CLOUD_CLIENT_SECRET} - NOTION_MANAGEMENT_CLOUD_CLIENT_ID: ${NOTION_MANAGEMENT_CLOUD_CLIENT_ID} - NOTION_MANAGEMENT_CLOUD_CLIENT_SECRET: ${NOTION_MANAGEMENT_CLOUD_CLIENT_SECRET} - SLACK_MANAGEMENT_CLOUD_CLIENT_ID: ${SLACK_MANAGEMENT_CLOUD_CLIENT_ID} - SLACK_MANAGEMENT_CLOUD_CLIENT_SECRET: ${SLACK_MANAGEMENT_CLOUD_CLIENT_SECRET} + NOTION_PRODUCTIVITY_CLOUD_CLIENT_ID: ${NOTION_PRODUCTIVITY_CLOUD_CLIENT_ID} + NOTION_PRODUCTIVITY_CLOUD_CLIENT_SECRET: ${NOTION_PRODUCTIVITY_CLOUD_CLIENT_SECRET} + SLACK_PRODUCTIVITY_CLOUD_CLIENT_ID: ${SLACK_PRODUCTIVITY_CLOUD_CLIENT_ID} + SLACK_PRODUCTIVITY_CLOUD_CLIENT_SECRET: ${SLACK_PRODUCTIVITY_CLOUD_CLIENT_SECRET} NAMELY_HRIS_CLOUD_CLIENT_ID: ${NAMELY_HRIS_CLOUD_CLIENT_ID} NAMELY_HRIS_CLOUD_CLIENT_SECRET: ${NAMELY_HRIS_CLOUD_CLIENT_SECRET} NAMELY_HRIS_CLOUD_SUBDOMAIN: ${NAMELY_HRIS_CLOUD_SUBDOMAIN} @@ -276,22 +276,22 @@ services: # volumes: # - pgadmin-data:/var/lib/pgadmin - ngrok: - image: ngrok/ngrok:latest - restart: always - command: - - "start" - - "--all" - - "--config" - - "/etc/ngrok.yml" - volumes: - - ./ngrok.yml:/etc/ngrok.yml - ports: - - 4040:4040 - depends_on: - api: - condition: service_healthy - network_mode: "host" + # ngrok: + # image: ngrok/ngrok:latest + # restart: always + # command: + # - "start" + # - "--all" + # - "--config" + # - "/etc/ngrok.yml" + # volumes: + # - ./ngrok.yml:/etc/ngrok.yml + # ports: + # - 4040:4040 + # depends_on: + # api: + # condition: service_healthy + # network_mode: "host" docs: build: diff --git a/docker-compose.source.yml b/docker-compose.source.yml index 69e6218cc..ba207adf0 100644 --- a/docker-compose.source.yml +++ b/docker-compose.source.yml @@ -150,10 +150,10 @@ services: FACTORIAL_HRIS_CLOUD_CLIENT_SECRET: ${FACTORIAL_ATS_CLOUD_CLIENT_SECRET} PAYFIT_HRIS_CLOUD_CLIENT_ID: ${PAYFIT_HRIS_CLOUD_CLIENT_ID} PAYFIT_HRIS_CLOUD_CLIENT_SECRET: ${PAYFIT_HRIS_CLOUD_CLIENT_SECRET} - NOTION_MANAGEMENT_CLOUD_CLIENT_ID: ${NOTION_MANAGEMENT_CLOUD_CLIENT_ID} - NOTION_MANAGEMENT_CLOUD_CLIENT_SECRET: ${NOTION_MANAGEMENT_CLOUD_CLIENT_SECRET} - SLACK_MANAGEMENT_CLOUD_CLIENT_ID: ${SLACK_MANAGEMENT_CLOUD_CLIENT_ID} - SLACK_MANAGEMENT_CLOUD_CLIENT_SECRET: ${SLACK_MANAGEMENT_CLOUD_CLIENT_SECRET} + NOTION_PRODUCTIVITY_CLOUD_CLIENT_ID: ${NOTION_PRODUCTIVITY_CLOUD_CLIENT_ID} + NOTION_PRODUCTIVITY_CLOUD_CLIENT_SECRET: ${NOTION_PRODUCTIVITY_CLOUD_CLIENT_SECRET} + SLACK_PRODUCTIVITY_CLOUD_CLIENT_ID: ${SLACK_PRODUCTIVITY_CLOUD_CLIENT_ID} + SLACK_PRODUCTIVITY_CLOUD_CLIENT_SECRET: ${SLACK_PRODUCTIVITY_CLOUD_CLIENT_SECRET} NAMELY_HRIS_CLOUD_CLIENT_ID: ${NAMELY_HRIS_CLOUD_CLIENT_ID} NAMELY_HRIS_CLOUD_CLIENT_SECRET: ${NAMELY_HRIS_CLOUD_CLIENT_SECRET} NAMELY_HRIS_CLOUD_SUBDOMAIN: ${NAMELY_HRIS_CLOUD_SUBDOMAIN} diff --git a/docker-compose.yml b/docker-compose.yml index ac010524a..b03be3f08 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -144,10 +144,10 @@ services: FACTORIAL_HRIS_CLOUD_CLIENT_SECRET: ${FACTORIAL_ATS_CLOUD_CLIENT_SECRET} PAYFIT_HRIS_CLOUD_CLIENT_ID: ${PAYFIT_HRIS_CLOUD_CLIENT_ID} PAYFIT_HRIS_CLOUD_CLIENT_SECRET: ${PAYFIT_HRIS_CLOUD_CLIENT_SECRET} - NOTION_MANAGEMENT_CLOUD_CLIENT_ID: ${NOTION_MANAGEMENT_CLOUD_CLIENT_ID} - NOTION_MANAGEMENT_CLOUD_CLIENT_SECRET: ${NOTION_MANAGEMENT_CLOUD_CLIENT_SECRET} - SLACK_MANAGEMENT_CLOUD_CLIENT_ID: ${SLACK_MANAGEMENT_CLOUD_CLIENT_ID} - SLACK_MANAGEMENT_CLOUD_CLIENT_SECRET: ${SLACK_MANAGEMENT_CLOUD_CLIENT_SECRET} + NOTION_PRODUCTIVITY_CLOUD_CLIENT_ID: ${NOTION_PRODUCTIVITY_CLOUD_CLIENT_ID} + NOTION_PRODUCTIVITY_CLOUD_CLIENT_SECRET: ${NOTION_PRODUCTIVITY_CLOUD_CLIENT_SECRET} + SLACK_PRODUCTIVITY_CLOUD_CLIENT_ID: ${SLACK_PRODUCTIVITY_CLOUD_CLIENT_ID} + SLACK_PRODUCTIVITY_CLOUD_CLIENT_SECRET: ${SLACK_PRODUCTIVITY_CLOUD_CLIENT_SECRET} NAMELY_HRIS_CLOUD_CLIENT_ID: ${NAMELY_HRIS_CLOUD_CLIENT_ID} NAMELY_HRIS_CLOUD_CLIENT_SECRET: ${NAMELY_HRIS_CLOUD_CLIENT_SECRET} NAMELY_HRIS_CLOUD_SUBDOMAIN: ${NAMELY_HRIS_CLOUD_SUBDOMAIN} diff --git a/docs/catalog.mdx b/docs/catalog.mdx index 0a3e9158f..23ebd1de1 100644 --- a/docs/catalog.mdx +++ b/docs/catalog.mdx @@ -52,6 +52,18 @@ icon: album-collection } horizontal> + + } horizontal> + + + } horizontal> + + + } horizontal> + @@ -66,7 +78,7 @@ icon: album-collection + } horizontal> } horizontal> + + } horizontal> + + + + + } horizontal> + + + + diff --git a/docs/hris/overview.mdx b/docs/hris/overview.mdx new file mode 100644 index 000000000..b150243b0 --- /dev/null +++ b/docs/hris/overview.mdx @@ -0,0 +1,10 @@ +--- +title: 'Overview' +description: '' +--- +import hrisCatalog from '/snippets/hris-catalog.mdx'; + +Welcome to the reference documentation for the Panora HRIS API! This API allows you to integrate with Panora and read data from or write data into the integrations authorized by your users. + +## Supported HRIS Providers & Objects + \ No newline at end of file diff --git a/docs/hris/quickstart.mdx b/docs/hris/quickstart.mdx new file mode 100644 index 000000000..d46c93e4f --- /dev/null +++ b/docs/hris/quickstart.mdx @@ -0,0 +1,89 @@ +--- +title: "Quick Start" +description: "Read and write data to multiple HRIS platforms using a single API" +icon: "star" +--- + +## Get employees in an HRIS using Panora + + + We assume for this tutorial that you have a valid Panora API Key, and a + `connection_token`. Find help [here](/core-concepts/auth). + + + + + You can find the Typescript SDK [here](https://www.npmjs.com/package/@panora/sdk-typescript) + + + + ```javascript TypeScript SDK + import { Panora } from '@panora/sdk'; + const panora = new Panora({ apiKey: process.env.API_KEY }); + ``` + + ```python Python SDK + import os + from panora_sdk import Panora + panora = Panora( + api_key=os.getenv("API_KEY", ""), + ) + ``` + + + + + In this example, we will get employees in an HRIS. Visit other sections of the documentation to find category-specific examples + + + ```shell curl + curl --request GET \ + --url https://api.panora.dev/hris/employees \ + --header 'x-api-key: ' \ + --header 'Content-Type: application/json' \ + --header 'x-connection-token: ' \ + ``` + + ```javascript TypeScript + import { Panora } from "@panora/sdk"; + + const panora = new Panora({ + apiKey: process.env.API_KEY, + }); + + const result = await panora.hris.employees.list({ + xConnectionToken: "", + remoteData: true, + limit: 10, + cursor: "1b8b05bb-5273-4012-b520-8657b0b90874", + }); + + for await (const page of result) { + // handle page + } + + console.log(result); + ``` + + ```python Python + import os + from panora_sdk import Panora + + panora = Panora( + api_key=os.getenv("API_KEY", ""), + ) + + res = panora.ticketing.tickets.list(x_connection_token="", remote_data=True, limit=10, cursor="1b8b05bb-5273-4012-b520-8657b0b90874") + + if res is not None: + while True: + # handle items + + res = res.Next() + if res is None: + break + ``` + + + + diff --git a/docs/integrations/ecommerce/amazon/index.mdx b/docs/integrations/ecommerce/amazon/index.mdx new file mode 100644 index 000000000..c4cbdfb6b --- /dev/null +++ b/docs/integrations/ecommerce/amazon/index.mdx @@ -0,0 +1,21 @@ +--- +title: "Amazon" +description: "" +--- + +# Common Objects + + +| Unified Model | Supported | Provider Endpoints | +| -------- | ------------------------------------- | ------------------------------------- | +| Customer | Yes ✅ | /admin/api/2024-07/customers.json | +| Order | Yes ✅ | /admin/api/2024-07/orders.json | +| Fulfillment | Yes ✅ | /admin/api/2024-07/orders/:remote_order_id/fulfillments.json| +| Fulfillment Orders | No 🚫| | +| Product | Yes ✅ | /admin/api/2024-07/products.json | + + +| Features | Supported | +| -------- | ------------------------------------- | +| Scopes | | +| Realtime webhook | No 🚫| \ No newline at end of file diff --git a/docs/integrations/ecommerce/index.mdx b/docs/integrations/ecommerce/index.mdx index 931fa1f19..c25b37ebf 100644 --- a/docs/integrations/ecommerce/index.mdx +++ b/docs/integrations/ecommerce/index.mdx @@ -9,4 +9,16 @@ icon: crab } horizontal> + + } horizontal> + + + } horizontal> + + + } horizontal> + diff --git a/docs/integrations/ecommerce/squarespace/index.mdx b/docs/integrations/ecommerce/squarespace/index.mdx new file mode 100644 index 000000000..f7ddf2d7b --- /dev/null +++ b/docs/integrations/ecommerce/squarespace/index.mdx @@ -0,0 +1,21 @@ +--- +title: "Squarespace" +description: "" +--- + +# Common Objects + + +| Unified Model | Supported | Provider Endpoints | +| -------- | ------------------------------------- | ------------------------------------- | +| Customer | Yes ✅ | /admin/api/2024-07/customers.json | +| Order | Yes ✅ | /admin/api/2024-07/orders.json | +| Fulfillment | Yes ✅ | /admin/api/2024-07/orders/:remote_order_id/fulfillments.json| +| Fulfillment Orders | No 🚫| | +| Product | Yes ✅ | /admin/api/2024-07/products.json | + + +| Features | Supported | +| -------- | ------------------------------------- | +| Scopes | | +| Realtime webhook | No 🚫| \ No newline at end of file diff --git a/docs/integrations/ecommerce/woocommerce/index.mdx b/docs/integrations/ecommerce/woocommerce/index.mdx new file mode 100644 index 000000000..0931aeca0 --- /dev/null +++ b/docs/integrations/ecommerce/woocommerce/index.mdx @@ -0,0 +1,21 @@ +--- +title: "Woocommerce" +description: "" +--- + +# Common Objects + + +| Unified Model | Supported | Provider Endpoints | +| -------- | ------------------------------------- | ------------------------------------- | +| Customer | Yes ✅ | /admin/api/2024-07/customers.json | +| Order | Yes ✅ | /admin/api/2024-07/orders.json | +| Fulfillment | Yes ✅ | /admin/api/2024-07/orders/:remote_order_id/fulfillments.json| +| Fulfillment Orders | No 🚫| | +| Product | Yes ✅ | /admin/api/2024-07/products.json | + + +| Features | Supported | +| -------- | ------------------------------------- | +| Scopes | | +| Realtime webhook | No 🚫| \ No newline at end of file diff --git a/docs/integrations/hris/gusto/index.mdx b/docs/integrations/hris/gusto/index.mdx new file mode 100644 index 000000000..aecd42b50 --- /dev/null +++ b/docs/integrations/hris/gusto/index.mdx @@ -0,0 +1,30 @@ +--- +title: "Gusto" +description: "" +--- + +# Common Objects + +| Unified Model | Supported | Provider Endpoints | +| ---------------- | --------- | ------------------ | +| Bankinfo | No 🚫 | | +| Benefit | Yes ✅ | /v1/employees/${employee.remote_id}/employee_benefits | +| Company | Yes ✅ | /v1/companies/${id} | +| Dependent | No 🚫 | | +| Employee | Yes ✅ | /v1/companies/${company.remote_id}/employees | +| Employeepayrollrun | No 🚫 | | +| Employerbenefit | Yes ✅ | /v1/companies/${company.remote_id}/company_benefits | +| Employment | Yes ✅ | /v1/companies/${company.remote_id}/employees | +| Group | Yes ✅ | /v1/companies/${company.remote_id}/departments | +| Location | Yes ✅ | [/v1/employees/${employee.remote_id}/work_addresses , /v1/employees/${employee.remote_id}/home_addresses] | +| Paygroup | No 🚫 | | +| Payrollrun | No 🚫 | | +| Timeoff | No 🚫 | | +| Timeoffbalance | No 🚫 | | +| Timesheetentry | No 🚫 | | + + +| Features | Supported | +| -------- | ------------------------------------- | +| Scopes || +| Realtime webhook | No 🚫| \ No newline at end of file diff --git a/docs/integrations/hris/index.mdx b/docs/integrations/hris/index.mdx new file mode 100644 index 000000000..8a01ce2eb --- /dev/null +++ b/docs/integrations/hris/index.mdx @@ -0,0 +1,12 @@ +--- +title: "Providers" +description: "" +icon: crab +--- + + + + } horizontal> + + diff --git a/docs/integrations/ticketing/github/index.mdx b/docs/integrations/ticketing/github/index.mdx new file mode 100644 index 000000000..f4ff6ee37 --- /dev/null +++ b/docs/integrations/ticketing/github/index.mdx @@ -0,0 +1,25 @@ +--- +title: "Github" +description: "" +--- + +# Common Objects + + +| Unified Model | Supported | Provider Endpoints | +| -------- | ------------------------------------- | ------------------------------------- | +| Account | Yes ✅ | /accounts | +| Attachment | No 🚫 | Coming Soon | +| Collection |No 🚫 | | +| Comment | Yes ✅| /conversations/:remote_ticket_id/comments | +| Contact | Yes ✅ | /contacts | +| Tag | Yes ✅| /tags | +| Team | Yes ✅| /teams | +| Ticket | Yes ✅ | /conversations | +| User | Yes ✅| /teammates| + + +| Features | Supported | +| -------- | ------------------------------------- | +| Scopes | | +| Realtime webhook | No 🚫| \ No newline at end of file diff --git a/docs/integrations/ticketing/index.mdx b/docs/integrations/ticketing/index.mdx index 1ea6c979c..0f66b1a37 100644 --- a/docs/integrations/ticketing/index.mdx +++ b/docs/integrations/ticketing/index.mdx @@ -21,4 +21,8 @@ icon: crab } horizontal> + + } horizontal> + diff --git a/docs/mint.json b/docs/mint.json index b893a8287..5456ae6ec 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -135,6 +135,13 @@ } ] }, + { + "group": "", + "pages": [ + "hris/quickstart", + "hris/overview" + ] + }, { "group": "", "pages": [ @@ -915,6 +922,10 @@ "name": "E-Commerce", "url": "ecommerce" }, + { + "name": "HRIS", + "url": "hris" + }, { "name": "File Storage", "url": "file-storage" diff --git a/docs/open-source/self_hosting/envVariables.mdx b/docs/open-source/self_hosting/envVariables.mdx index 7bfb33e6e..efb5e7a60 100644 --- a/docs/open-source/self_hosting/envVariables.mdx +++ b/docs/open-source/self_hosting/envVariables.mdx @@ -159,10 +159,10 @@ description: "" | MAILCHIMP_MARKETINGAUTOMATION_CLOUD_CLIENT_SECRET | | | | KLAVIYO_TICKETING_CLOUD_CLIENT_ID | | | | KLAVIYO_TICKETING_CLOUD_CLIENT_SECRET | | | -| NOTION_MANAGEMENT_CLOUD_CLIENT_ID | | | -| NOTION_MANAGEMENT_CLOUD_CLIENT_SECRET | | | -| SLACK_MANAGEMENT_CLOUD_CLIENT_ID | | | -| SLACK_MANAGEMENT_CLOUD_CLIENT_SECRET | | | +| NOTION_PRODUCTIVITY_CLOUD_CLIENT_ID | | | +| NOTION_PRODUCTIVITY_CLOUD_CLIENT_SECRET | | | +| SLACK_PRODUCTIVITY_CLOUD_CLIENT_ID | | | +| SLACK_PRODUCTIVITY_CLOUD_CLIENT_SECRET | | | | GREENHOUSE_ATS_CLOUD_CLIENT_ID | | | | GREENHOUSE_ATS_CLOUD_CLIENT_SECRET | | | | JOBADDER_ATS_CLOUD_CLIENT_ID | | | diff --git a/docs/snippets/hris-catalog.mdx b/docs/snippets/hris-catalog.mdx new file mode 100644 index 000000000..9fc70aee0 --- /dev/null +++ b/docs/snippets/hris-catalog.mdx @@ -0,0 +1,3 @@ +| | Bankinfo | Benefit | Company | Dependent | Employee | Employeepayrollrun | Employerbenefit | Employment | Group | Location | Paygroup | Payrollrun | Timeoff | Timeoffbalance | Timesheetentry | +|-------------|----------|:-------:|:-------:|:---------:|:--------:|:------------------:|:---------------:|:----------:|:-----:|:--------:|:--------:|:----------:|:-------:|:--------------:|:---------------:| +| Gusto | ❌ | ✔ | ✔ | ❌ | ✔ | ❌ | ✔ | ✔ | ✔ | ✔ | ❌ | ❌ | ❌ | ❌ | ❌ | \ No newline at end of file diff --git a/docs/syncwithCode.sh b/docs/syncwithCode.sh index a9feea75c..fcfa30015 100644 --- a/docs/syncwithCode.sh +++ b/docs/syncwithCode.sh @@ -14,7 +14,7 @@ grep '^|' ../packages/api/src/ats/README.md > snippets/ats-catalog.mdx # File Storage grep '^|' ../packages/api/src/filestorage/README.md > snippets/filestorage-catalog.mdx -# File Storage +# Ecommerce grep '^|' ../packages/api/src/ecommerce/README.md > snippets/ecommerce-catalog.mdx npx @mintlify/scraping@latest openapi-file openapi-with-code-samples.yaml -o objects diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index 795f6a6c7..58997c432 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -8,768 +8,595 @@ datasource db { } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model users { - id_user String @id(map: "pk_users") @db.Uuid - identification_strategy String - email String? @unique(map: "unique_email") - password_hash String? - first_name String - last_name String - id_stytch String? @unique(map: "force_stytch_id_unique") - created_at DateTime @default(now()) @db.Timestamp(6) - modified_at DateTime @default(now()) @db.Timestamp(6) - reset_token String? - reset_token_expires_at DateTime? @db.Timestamptz(6) - api_keys api_keys[] - projects projects[] +model acc_accounting_periods { + id_acc_accounting_period String @id(map: "pk_acc_accounting_periods") @db.Uuid + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + name String? + status String? + start_date DateTime? @db.Timestamptz(6) + end_date DateTime? @db.Timestamptz(6) + id_connection String @db.Uuid } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model webhook_endpoints { - id_webhook_endpoint String @id(map: "pk_webhook_endpoint") @db.Uuid - endpoint_description String? - url String - secret String - active Boolean - created_at DateTime @db.Timestamp(6) - scope String[] - id_project String @db.Uuid - last_update DateTime? @db.Timestamp(6) - webhook_delivery_attempts webhook_delivery_attempts[] -} +model acc_accounts { + id_acc_account String @id(map: "pk_acc_accounts") @db.Uuid + name String? + description String? + classification String? + type String? + status String? + current_balance BigInt? + currency String? + account_number String? + parent_account String? @db.Uuid + remote_id String? + id_acc_company_info String? @db.Uuid + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid -model webhooks_payloads { - id_webhooks_payload String @id(map: "pk_webhooks_payload") @db.Uuid - data Json @db.Json - webhook_delivery_attempts webhook_delivery_attempts[] + @@index([id_acc_company_info], map: "fk_accounts_companyinfo_id") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model webhooks_reponses { - id_webhooks_reponse String @id(map: "pk_webhooks_reponse") @db.Uuid - http_response_data String - http_status_code String - webhook_delivery_attempts webhook_delivery_attempts[] +model acc_addresses { + id_acc_address String @id(map: "pk_acc_addresses") @db.Uuid + type String? + street_1 String? + street_2 String? + city String? + remote_id String? + state String? + country_subdivision String? + country String? + zip String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_acc_contact String? @db.Uuid + id_acc_company_info String? @db.Uuid + id_connection String @db.Uuid + + @@index([id_acc_company_info], map: "fk_acc_company_info_acc_adresses") + @@index([id_acc_contact], map: "fk_acc_contact_acc_addresses") } -model api_keys { - id_api_key String @id(map: "id_") @db.Uuid - api_key_hash String @unique(map: "unique_api_keys") - name String? - id_project String @db.Uuid - id_user String @db.Uuid - projects projects @relation(fields: [id_project], references: [id_project], onDelete: NoAction, onUpdate: NoAction, map: "fk_7") - users users @relation(fields: [id_user], references: [id_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_8") +model acc_attachments { + id_acc_attachment String @id(map: "pk_acc_attachments") @db.Uuid + file_name String? + file_url String? + remote_id String? + id_acc_account String? @db.Uuid + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid - @@index([id_project], map: "fk_api_keys_projects") - @@index([id_user], map: "fk_2") + @@index([id_acc_account], map: "fk_acc_attachments_accountid") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model attribute { - id_attribute String @id(map: "pk_attribute") @db.Uuid - status String - ressource_owner_type String - slug String - description String - data_type String - remote_id String - source String - id_entity String? @db.Uuid - id_project String @db.Uuid - scope String - id_consumer String? @db.Uuid - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - entity entity? @relation(fields: [id_entity], references: [id_entity], onDelete: NoAction, onUpdate: NoAction, map: "fk_32") - value value[] +model acc_balance_sheets { + id_acc_balance_sheet String @id(map: "pk_acc_balance_sheets") @db.Uuid + name String? + currency String? + id_acc_company_info String? @db.Uuid + date DateTime? @db.Timestamptz(6) + net_assets BigInt? + assets String[] + liabilities String[] + equity String[] + remote_generated_at DateTime? @db.Timestamptz(6) + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid - @@index([id_entity], map: "fk_attribute_entityid") + @@index([id_acc_company_info], map: "fk_balancesheetcompanyinfoid") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model connection_strategies { - id_connection_strategy String @id(map: "pk_connection_strategies") @db.Uuid - status Boolean - type String - id_project String? @db.Uuid +model acc_balance_sheets_report_items { + id_acc_balance_sheets_report_item String @id(map: "pk_acc_balance_sheets_report_items") @db.Uuid + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + name String? + value BigInt? + parent_item String? @db.Uuid + id_acc_company_info String? @db.Uuid } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model connections { - id_connection String @id(map: "pk_connections") @db.Uuid - status String - provider_slug String - vertical String - account_url String? - token_type String - access_token String? - refresh_token String? - expiration_timestamp DateTime? @db.Timestamp(6) - created_at DateTime @db.Timestamp(6) - connection_token String? - id_project String @db.Uuid - id_linked_user String @db.Uuid - linked_users linked_users @relation(fields: [id_linked_user], references: [id_linked_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_11") - projects projects @relation(fields: [id_project], references: [id_project], onDelete: NoAction, onUpdate: NoAction, map: "fk_9") +model acc_cash_flow_statement_report_items { + id_acc_cash_flow_statement_report_item String @id(map: "pk_acc_cash_flow_statement_report_items") @db.Uuid + name String? + value BigInt? + type String? + parent_item String? @db.Uuid + remote_generated_at DateTime? @db.Timestamptz(6) + remote_id String? + modified_at DateTime @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + id_acc_cash_flow_statement String? @db.Uuid - @@index([id_project], map: "fk_1") - @@index([id_linked_user], map: "fk_connections_to_linkedusersid") + @@index([id_acc_cash_flow_statement], map: "fk_cashflow_statement_acc_cash_flow_statement_report_item") } -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model crm_addresses { - id_crm_address String @id(map: "pk_crm_addresses") @db.Uuid - street_1 String? - street_2 String? - city String? - state String? - postal_code String? - country String? - address_type String? - id_crm_company String? @db.Uuid - id_crm_contact String? @db.Uuid - id_connection String @db.Uuid - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - owner_type String - crm_contacts crm_contacts? @relation(fields: [id_crm_contact], references: [id_crm_contact], onDelete: NoAction, onUpdate: NoAction, map: "fk_14") - crm_companies crm_companies? @relation(fields: [id_crm_company], references: [id_crm_company], onDelete: NoAction, onUpdate: NoAction, map: "fk_15") +model acc_cash_flow_statements { + id_acc_cash_flow_statement String @id(map: "pk_acc_cash_flow_statements") @db.Uuid + name String? + currency String? + company String? @db.Uuid + start_period DateTime? @db.Timestamptz(6) + end_period DateTime? @db.Timestamptz(6) + cash_at_beginning_of_period BigInt? + cash_at_end_of_period BigInt? + remote_generated_at DateTime? @db.Timestamptz(6) + remote_id String? + modified_at DateTime @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid +} - @@index([id_crm_contact], map: "fk_crm_addresses_to_crm_contacts") - @@index([id_crm_company], map: "fk_crm_adresses_to_crm_companies") +model acc_company_infos { + id_acc_company_info String @id(map: "pk_acc_company_infos") @db.Uuid + name String? + legal_name String? + tax_number String? + fiscal_year_end_month Int? + fiscal_year_end_day Int? + currency String? + remote_created_at DateTime? @db.Timestamptz(6) + remote_id String? + urls String[] + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid + tracking_categories String[] } -model crm_companies { - id_crm_company String @id(map: "pk_crm_companies") @db.Uuid +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model acc_contacts { + id_acc_contact String @id(map: "pk_acc_contacts") @db.Uuid name String? - industry String? - number_of_employees BigInt? - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) + is_supplier Boolean? + is_customer Boolean? + email_address String? + tax_number String? + status String? + currency String? + remote_updated_at String? + id_acc_company_info String? @db.Uuid + id_connection String @db.Uuid remote_id String? - remote_platform String? - id_crm_user String? @db.Uuid - id_linked_user String? @db.Uuid - id_connection String @db.Uuid - crm_addresses crm_addresses[] - crm_users crm_users? @relation(fields: [id_crm_user], references: [id_crm_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_24") - crm_deals crm_deals[] - crm_email_addresses crm_email_addresses[] - crm_engagements crm_engagements[] - crm_notes crm_notes[] - crm_phone_numbers crm_phone_numbers[] - crm_tasks crm_tasks[] + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) - @@index([id_crm_user], map: "fk_crm_company_crm_userid") + @@index([id_acc_company_info], map: "fk_acc_contact_company") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model crm_contacts { - id_crm_contact String @id(map: "pk_crm_contacts") @db.Uuid - first_name String? - last_name String? - created_at DateTime? @db.Timestamp(6) - modified_at DateTime? @db.Timestamp(6) - remote_id String? - remote_platform String? - id_crm_user String? @db.Uuid - id_linked_user String? @db.Uuid - id_connection String @db.Uuid - crm_addresses crm_addresses[] - crm_users crm_users? @relation(fields: [id_crm_user], references: [id_crm_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_23") - crm_email_addresses crm_email_addresses[] - crm_notes crm_notes[] - crm_phone_numbers crm_phone_numbers[] - - @@index([id_crm_user], map: "fk_crm_contact_userid") -} - -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model crm_deals { - id_crm_deal String @id(map: "pk_crm_deal") @db.Uuid - name String - description String? - amount BigInt - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - remote_id String? - remote_platform String? - id_crm_user String? @db.Uuid - id_crm_deals_stage String? @db.Uuid - id_linked_user String? @db.Uuid - id_crm_company String? @db.Uuid - id_connection String @db.Uuid - crm_deals_stages crm_deals_stages? @relation(fields: [id_crm_deals_stage], references: [id_crm_deals_stage], onDelete: NoAction, onUpdate: NoAction, map: "fk_21") - crm_users crm_users? @relation(fields: [id_crm_user], references: [id_crm_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_22") - crm_companies crm_companies? @relation(fields: [id_crm_company], references: [id_crm_company], onDelete: NoAction, onUpdate: NoAction, map: "fk_47_1") - crm_notes crm_notes[] - crm_tasks crm_tasks[] - - @@index([id_crm_user], map: "crm_deal_crm_userid") - @@index([id_crm_deals_stage], map: "crm_deal_deal_stageid") - @@index([id_crm_company], map: "fk_crm_deal_crmcompanyid") -} - -model crm_deals_stages { - id_crm_deals_stage String @id(map: "pk_crm_deal_stages") @db.Uuid - stage_name String? - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_linked_user String? @db.Uuid - remote_id String? - remote_platform String? - id_connection String @db.Uuid - crm_deals crm_deals[] -} - -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model crm_email_addresses { - id_crm_email String @id(map: "pk_crm_contact_email_addresses") @db.Uuid - email_address String - email_address_type String - owner_type String - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_crm_company String? @db.Uuid - id_crm_contact String? @db.Uuid - id_connection String @db.Uuid - crm_companies crm_companies? @relation(fields: [id_crm_company], references: [id_crm_company], onDelete: NoAction, onUpdate: NoAction, map: "fk_16") - crm_contacts crm_contacts? @relation(fields: [id_crm_contact], references: [id_crm_contact], onDelete: NoAction, onUpdate: NoAction, map: "fk_3") - - @@index([id_crm_contact], map: "crm_contactid_crm_contact_email_address") - @@index([id_crm_company], map: "fk_contact_email_adress_companyid") -} - -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model crm_engagements { - id_crm_engagement String @id(map: "pk_crm_engagement") @db.Uuid - content String? - type String? - direction String? - subject String? - start_at DateTime? @db.Timestamp(6) - end_time DateTime? @db.Timestamp(6) - remote_id String? - id_linked_user String? @db.Uuid - remote_platform String? - id_crm_company String? @db.Uuid - id_crm_user String? @db.Uuid - id_connection String @db.Uuid - contacts String[] - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - crm_companies crm_companies? @relation(fields: [id_crm_company], references: [id_crm_company], onDelete: NoAction, onUpdate: NoAction, map: "fk_29") - crm_users crm_users? @relation(fields: [id_crm_user], references: [id_crm_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_crm_engagement_crm_user") - - @@index([id_crm_user], map: "fk_crm_engagement_crm_user_id") - @@index([id_crm_company], map: "fk_crm_engagement_crmcompanyid") -} - -model crm_notes { - id_crm_note String @id(map: "pk_crm_notes") @db.Uuid - content String - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_crm_company String? @db.Uuid - id_crm_contact String? @db.Uuid - id_crm_deal String? @db.Uuid - id_linked_user String? @db.Uuid - remote_id String? - remote_platform String? - id_crm_user String? @db.Uuid - id_connection String @db.Uuid - crm_companies crm_companies? @relation(fields: [id_crm_company], references: [id_crm_company], onDelete: NoAction, onUpdate: NoAction, map: "fk_18") - crm_contacts crm_contacts? @relation(fields: [id_crm_contact], references: [id_crm_contact], onDelete: NoAction, onUpdate: NoAction, map: "fk_19") - crm_deals crm_deals? @relation(fields: [id_crm_deal], references: [id_crm_deal], onDelete: NoAction, onUpdate: NoAction, map: "fk_20") - - @@index([id_crm_contact], map: "fk_crm_note_crm_companyid") - @@index([id_crm_company], map: "fk_crm_note_crm_contactid") - @@index([id_crm_user], map: "fk_crm_note_crm_userid") - @@index([id_crm_deal], map: "fk_crm_notes_crm_dealid") -} - -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model crm_phone_numbers { - id_crm_phone_number String @id(map: "pk_crm_contacts_phone_numbers") @db.Uuid - phone_number String? - phone_type String? - owner_type String? - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_crm_company String? @db.Uuid - id_crm_contact String? @db.Uuid - id_connection String @db.Uuid - crm_companies crm_companies? @relation(fields: [id_crm_company], references: [id_crm_company], onDelete: NoAction, onUpdate: NoAction, map: "fk_17") - crm_contacts crm_contacts? @relation(fields: [id_crm_contact], references: [id_crm_contact], onDelete: NoAction, onUpdate: NoAction, map: "fk_phonenumber_crm_contactid") - - @@index([id_crm_contact], map: "crm_contactid_crm_contact_phone_number") - @@index([id_crm_company], map: "fk_phone_number_companyid") -} - -model crm_tasks { - id_crm_task String @id(map: "pk_crm_task") @db.Uuid - subject String? - content String? - status String? - due_date DateTime? @db.Timestamp(6) - finished_date DateTime? @db.Timestamp(6) - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_crm_user String? @db.Uuid - id_crm_company String? @db.Uuid - id_crm_deal String? @db.Uuid - id_linked_user String? @db.Uuid - remote_id String? - remote_platform String? - id_connection String @db.Uuid - crm_users crm_users? @relation(fields: [id_crm_user], references: [id_crm_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_25") - crm_companies crm_companies? @relation(fields: [id_crm_company], references: [id_crm_company], onDelete: NoAction, onUpdate: NoAction, map: "fk_26") - crm_deals crm_deals? @relation(fields: [id_crm_deal], references: [id_crm_deal], onDelete: NoAction, onUpdate: NoAction, map: "fk_27") - - @@index([id_crm_company], map: "fk_crm_task_companyid") - @@index([id_crm_user], map: "fk_crm_task_userid") - @@index([id_crm_deal], map: "fk_crmtask_dealid") -} - -model crm_users { - id_crm_user String @id(map: "pk_crm_users") @db.Uuid - name String? - email String? - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_linked_user String? @db.Uuid - remote_id String? - remote_platform String? - id_connection String @db.Uuid - crm_companies crm_companies[] - crm_contacts crm_contacts[] - crm_deals crm_deals[] - crm_engagements crm_engagements[] - crm_tasks crm_tasks[] -} - -model cs_attributes { - id_cs_attribute String @id(map: "pk_ct_attributes") @db.Uuid - attribute_slug String - data_type String - id_cs_entity String @db.Uuid -} - -model cs_entities { - id_cs_entity String @id(map: "pk_ct_entities") @db.Uuid - id_connection_strategy String @db.Uuid -} - -model cs_values { - id_cs_value String @id(map: "pk_ct_values") @db.Uuid - value String - id_cs_attribute String @db.Uuid -} - -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model entity { - id_entity String @id(map: "pk_entity") @db.Uuid - ressource_owner_id String @db.Uuid - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - attribute attribute[] - value value[] -} - -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model events { - id_event String @id(map: "pk_jobs") @db.Uuid - id_connection String @db.Uuid - id_project String @db.Uuid - type String - status String - direction String - method String - url String - provider String - timestamp DateTime @default(now()) @db.Timestamp(6) - id_linked_user String @db.Uuid - linked_users linked_users @relation(fields: [id_linked_user], references: [id_linked_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_12") - jobs_status_history jobs_status_history[] - webhook_delivery_attempts webhook_delivery_attempts[] - - @@index([id_linked_user], map: "fk_linkeduserid_projectid") -} - -model invite_links { - id_invite_link String @id(map: "pk_invite_links") @db.Uuid - status String - email String? - id_linked_user String @db.Uuid - linked_users linked_users @relation(fields: [id_linked_user], references: [id_linked_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_37") - - @@index([id_linked_user], map: "fk_invite_link_linkeduserid") -} - -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model jobs_status_history { - id_jobs_status_history String @id(map: "pk_jobs_status_history") @db.Uuid - timestamp DateTime @default(now()) @db.Timestamp(6) - previous_status String - new_status String - id_event String @db.Uuid - events events @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_4") - - @@index([id_event], map: "id_job_jobs_status_history") +model acc_credit_notes { + id_acc_credit_note String @id(map: "pk_acc_credit_notes") @db.Uuid + transaction_date DateTime? @db.Timestamptz(6) + status String? + number String? + id_acc_contact String? @db.Uuid + company String? @db.Uuid + exchange_rate String? + total_amount BigInt? + remaining_credit BigInt? + tracking_categories String[] + currency String? + remote_created_at DateTime? @db.Timestamptz(6) + remote_updated_at DateTime? @db.Timestamptz(6) + payments String[] + applied_payments String[] + id_acc_accounting_period String? @db.Uuid + remote_id String? + modified_at DateTime @db.Timetz(6) + created_at DateTime @db.Timetz(6) + id_connection String @db.Uuid } -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model linked_users { - id_linked_user String @id(map: "key_id_linked_users") @db.Uuid - linked_user_origin_id String - alias String - id_project String @db.Uuid - connections connections[] - events events[] - invite_links invite_links[] - projects projects @relation(fields: [id_project], references: [id_project], onDelete: NoAction, onUpdate: NoAction, map: "fk_10") +model acc_expense_lines { + id_acc_expense_line String @id(map: "pk_acc_expense_lines") @db.Uuid + id_acc_expense String @db.Uuid + remote_id String? + net_amount BigInt? + currency String? + description String? + exchange_rate String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid - @@index([id_project], map: "fk_proectid_linked_users") + @@index([id_acc_expense], map: "fk_acc_expense_expense_lines_index") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model projects { - id_project String @id(map: "pk_projects") @db.Uuid - name String - sync_mode String - pull_frequency BigInt? - redirect_url String? - id_user String @db.Uuid - id_connector_set String @db.Uuid - api_keys api_keys[] - connections connections[] - linked_users linked_users[] - users users @relation(fields: [id_user], references: [id_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_46_1") - connector_sets connector_sets @relation(fields: [id_connector_set], references: [id_connector_set], onDelete: NoAction, onUpdate: NoAction, map: "fk_project_connectorsetid") - - @@index([id_connector_set], map: "fk_connectors_sets") -} +model acc_expenses { + id_acc_expense String @id(map: "pk_acc_expenses") @db.Uuid + transaction_date DateTime? @db.Timestamptz(6) + total_amount BigInt? + sub_total BigInt? + total_tax_amount BigInt? + currency String? + exchange_rate String? + memo String? + id_acc_account String? @db.Uuid + id_acc_contact String? @db.Uuid + id_acc_company_info String? @db.Uuid + remote_id String? + remote_created_at DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid + tracking_categories String[] -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model remote_data { - id_remote_data String @id(map: "pk_remote_data") @db.Uuid - ressource_owner_id String? @unique(map: "force_unique_ressourceownerid") @db.Uuid - format String? - data String? - created_at DateTime? @db.Timestamp(6) + @@index([id_acc_account], map: "fk_acc_account_acc_expense_index") + @@index([id_acc_company_info], map: "fk_acc_expense_acc_company_index") + @@index([id_acc_contact], map: "fk_acc_expense_acc_contact_index") } -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model tcg_accounts { - id_tcg_account String @id(map: "pk_tcg_account") @db.Uuid - remote_id String? - name String? - domains String[] - remote_platform String? - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_linked_user String? @db.Uuid - id_connection String @db.Uuid - tcg_contacts tcg_contacts[] +model acc_income_statements { + id_acc_income_statement String @id(map: "pk_acc_income_statements") @db.Uuid + name String? + currency String? + start_period DateTime? @db.Timestamptz(6) + end_period DateTime? @db.Timestamptz(6) + gross_profit BigInt? + net_operating_income BigInt? + net_income BigInt? + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model tcg_attachments { - id_tcg_attachment String @id(map: "pk_tcg_attachments") @db.Uuid - remote_id String? - remote_platform String? - file_name String? - file_url String? - uploader String @db.Uuid - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_linked_user String? @db.Uuid - id_tcg_ticket String? @db.Uuid - id_tcg_comment String? @db.Uuid - id_connection String @db.Uuid - tcg_tickets tcg_tickets? @relation(fields: [id_tcg_ticket], references: [id_tcg_ticket], onDelete: NoAction, onUpdate: NoAction, map: "fk_50") - tcg_comments tcg_comments? @relation(fields: [id_tcg_comment], references: [id_tcg_comment], onDelete: NoAction, onUpdate: NoAction, map: "fk_51") +model acc_invoices { + id_acc_invoice String @id(map: "pk_acc_invoices") @db.Uuid + type String? + number String? + issue_date DateTime? @db.Timestamptz(6) + due_date DateTime? @db.Timestamptz(6) + paid_on_date DateTime? @db.Timestamptz(6) + memo String? + currency String? + exchange_rate String? + total_discount BigInt? + sub_total BigInt? + status String? + total_tax_amount BigInt? + total_amount BigInt? + balance BigInt? + remote_updated_at DateTime? @db.Timestamptz(6) + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid + id_acc_contact String? @db.Uuid + id_acc_accounting_period String? @db.Uuid + tracking_categories String[] - @@index([id_tcg_comment], map: "fk_tcg_attachment_tcg_commentid") - @@index([id_tcg_ticket], map: "fk_tcg_attachment_tcg_ticketid") + @@index([id_acc_accounting_period], map: "fk_acc_invoice_accounting_period_index") + @@index([id_acc_contact], map: "fk_invoice_contactid") } -model tcg_collections { - id_tcg_collection String @id(map: "pk_tcg_collections") @db.Uuid - name String? - description String? - remote_id String? - remote_platform String? - collection_type String? - parent_collection String? @db.Uuid - id_tcg_ticket String? @db.Uuid - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_linked_user String @db.Uuid - id_connection String @db.Uuid +model acc_invoices_line_items { + id_acc_invoices_line_item String @id(map: "pk_acc_invoices_line_items") @db.Uuid + remote_id String? + description String? + unit_price BigInt? + quantity BigInt? + total_amount BigInt? + currency String? + exchange_rate String? + id_acc_invoice String @db.Uuid + id_acc_item String @db.Uuid + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid + acc_tracking_categories String[] + + @@index([id_acc_invoice], map: "fk_acc_invoice_line_items_index") + @@index([id_acc_item], map: "fk_acc_items_lines_invoice_index") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model tcg_comments { - id_tcg_comment String @id(map: "pk_tcg_comments") @db.Uuid - body String? - html_body String? - is_private Boolean? - remote_id String? - remote_platform String? - creator_type String? - id_tcg_attachment String[] - id_tcg_ticket String? @db.Uuid - id_tcg_contact String? @db.Uuid - id_tcg_user String? @db.Uuid - id_linked_user String? @db.Uuid - created_at DateTime? @db.Timestamp(6) - modified_at DateTime? @db.Timestamp(6) - id_connection String @db.Uuid - tcg_attachments tcg_attachments[] - tcg_tickets tcg_tickets? @relation(fields: [id_tcg_ticket], references: [id_tcg_ticket], onDelete: NoAction, onUpdate: NoAction, map: "fk_40_1") - tcg_contacts tcg_contacts? @relation(fields: [id_tcg_contact], references: [id_tcg_contact], onDelete: NoAction, onUpdate: NoAction, map: "fk_41") - tcg_users tcg_users? @relation(fields: [id_tcg_user], references: [id_tcg_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_42") +model acc_items { + id_acc_item String @id(map: "pk_acc_items") @db.Uuid + name String? + status String? + unit_price BigInt? + purchase_price BigInt? + remote_updated_at DateTime? @db.Timestamptz(6) + remote_id String? + sales_account String? @db.Uuid + purchase_account String? @db.Uuid + id_acc_company_info String? @db.Uuid + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid - @@index([id_tcg_contact], map: "fk_tcg_comment_tcg_contact") - @@index([id_tcg_ticket], map: "fk_tcg_comment_tcg_ticket") - @@index([id_tcg_user], map: "fk_tcg_comment_tcg_userid") + @@index([purchase_account], map: "fk_acc_item_acc_account") + @@index([id_acc_company_info], map: "fk_acc_item_acc_company_infos") + @@index([sales_account], map: "fk_acc_items_sales_account") } -model tcg_contacts { - id_tcg_contact String @id(map: "pk_tcg_contact") @db.Uuid - name String? - email_address String? - phone_number String? - details String? - remote_id String? - remote_platform String? - created_at DateTime? @db.Timestamp(6) - modified_at DateTime? @db.Timestamp(6) - id_tcg_account String? @db.Uuid - id_linked_user String? @db.Uuid - id_connection String @db.Uuid - tcg_comments tcg_comments[] - tcg_accounts tcg_accounts? @relation(fields: [id_tcg_account], references: [id_tcg_account], onDelete: NoAction, onUpdate: NoAction, map: "fk_49") +model acc_journal_entries { + id_acc_journal_entry String @id(map: "pk_acc_journal_entries") @db.Uuid + transaction_date DateTime? @db.Timestamptz(6) + payments String[] + applied_payments String[] + memo String? + currency String? + exchange_rate String? + id_acc_company_info String @db.Uuid + journal_number String? + tracking_categories String[] + id_acc_accounting_period String? @db.Uuid + posting_status String? + remote_created_at DateTime? @db.Timestamptz(6) + remote_modiified_at DateTime? @db.Timestamptz(6) + id_connection String @db.Uuid + remote_id String + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) - @@index([id_tcg_account], map: "fk_tcg_contact_tcg_account_id") + @@index([id_acc_accounting_period], map: "fk_journal_entry_accounting_period") + @@index([id_acc_company_info], map: "fk_journal_entry_companyinfo") } -model tcg_tags { - id_tcg_tag String @id(map: "pk_tcg_tags") @db.Uuid - name String? - remote_id String? - remote_platform String? - id_tcg_ticket String? @db.Uuid - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_linked_user String? @db.Uuid - id_connection String @db.Uuid - tcg_tickets tcg_tickets? @relation(fields: [id_tcg_ticket], references: [id_tcg_ticket], onDelete: NoAction, onUpdate: NoAction, map: "fk_48") +model acc_journal_entries_lines { + id_acc_journal_entries_line String @id(map: "pk_acc_journal_entries_lines") @db.Uuid + net_amount BigInt? + tracking_categories String[] + currency String? + description String? + company String? @db.Uuid + contact String? @db.Uuid + exchange_rate String? + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_acc_journal_entry String @db.Uuid - @@index([id_tcg_ticket], map: "fk_tcg_tag_tcg_ticketid") + @@index([id_acc_journal_entry], map: "fk_journal_entries_entries_lines") } -model tcg_teams { - id_tcg_team String @id(map: "pk_tcg_teams") @db.Uuid - remote_id String? - remote_platform String? - name String? - description String? - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_linked_user String? @db.Uuid - id_connection String @db.Uuid +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model acc_payments { + id_acc_payment String @id(map: "pk_acc_payments") @db.Uuid + id_acc_invoice String? @db.Uuid + transaction_date DateTime? @db.Timestamptz(6) + id_acc_contact String? @db.Uuid + id_acc_account String? @db.Uuid + currency String? + exchange_rate String? + total_amount BigInt? + remote_id String? + type String? + remote_updated_at DateTime? @db.Timestamptz(6) + id_acc_company_info String? @db.Uuid + id_acc_accounting_period String? @db.Uuid + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid + tracking_categories String[] + + @@index([id_acc_account], map: "fk_acc_payment_acc_account_index") + @@index([id_acc_company_info], map: "fk_acc_payment_acc_company_index") + @@index([id_acc_contact], map: "fk_acc_payment_acc_contact") + @@index([id_acc_accounting_period], map: "fk_acc_payment_accounting_period_index") + @@index([id_acc_invoice], map: "fk_acc_payment_invoiceid") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model tcg_tickets { - id_tcg_ticket String @id(map: "pk_tcg_tickets") @db.Uuid - name String? - status String? - description String? - due_date DateTime? @db.Timestamp(6) - ticket_type String? - parent_ticket String? @db.Uuid - tags String[] - collections String[] - completed_at DateTime? @db.Timestamp(6) - priority String? - assigned_to String[] - remote_id String? - remote_platform String? - creator_type String? - id_tcg_user String? @db.Uuid - id_linked_user String? @db.Uuid - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_connection String @db.Uuid - tcg_attachments tcg_attachments[] - tcg_comments tcg_comments[] - tcg_tags tcg_tags[] +model acc_payments_line_items { + acc_payments_line_item String @id(map: "pk_acc_payments_line_items") @db.Uuid + id_acc_payment String @db.Uuid + applied_amount BigInt? + applied_date DateTime? @db.Timestamptz(6) + related_object_id String? @db.Uuid + related_object_type String? + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid - @@index([id_tcg_user], map: "fk_tcg_ticket_tcg_user") + @@index([id_acc_payment], map: "fk_acc_payment_line_items_index") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model tcg_users { - id_tcg_user String @id(map: "pk_tcg_users") @db.Uuid - name String? - email_address String? - remote_id String? - remote_platform String? - teams String[] - id_linked_user String? @db.Uuid - id_connection String @db.Uuid - created_at DateTime? @db.Timestamp(6) - modified_at DateTime? @db.Timestamp(6) - tcg_comments tcg_comments[] +model acc_phone_numbers { + id_acc_phone_number String @id(map: "pk_acc_phone_numbers") @db.Uuid + number String? + type String? + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_acc_company_info String? @db.Uuid + id_acc_contact String @db.Uuid + id_connection String @db.Uuid + + @@index([id_acc_contact], map: "fk_acc_phone_number_contact") + @@index([id_acc_company_info], map: "fk_company_infos_phone_number") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model value { - id_value String @id(map: "pk_value") @db.Uuid - data String - id_entity String @db.Uuid - id_attribute String @db.Uuid - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - attribute attribute @relation(fields: [id_attribute], references: [id_attribute], onDelete: NoAction, onUpdate: NoAction, map: "fk_33") - entity entity @relation(fields: [id_entity], references: [id_entity], onDelete: NoAction, onUpdate: NoAction, map: "fk_34") +model acc_purchase_orders { + id_acc_purchase_order String @id(map: "pk_acc_purchase_orders") @db.Uuid + remote_id String? + status String? + issue_date DateTime? @db.Timestamptz(6) + purchase_order_number String? + delivery_date DateTime? @db.Timestamptz(6) + delivery_address String? @db.Uuid + customer String? @db.Uuid + vendor String? @db.Uuid + memo String? + company String? @db.Uuid + total_amount BigInt? + currency String? + exchange_rate String? + tracking_categories String[] + remote_created_at DateTime? @db.Timestamptz(6) + remote_updated_at DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid + id_acc_accounting_period String? @db.Uuid - @@index([id_attribute], map: "fk_value_attributeid") - @@index([id_entity], map: "fk_value_entityid") + @@index([id_acc_accounting_period], map: "fk_purchaseorder_accountingperiod") } -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model webhook_delivery_attempts { - id_webhook_delivery_attempt String @id(map: "pk_webhook_event") @db.Uuid - timestamp DateTime @db.Timestamp(6) - status String - next_retry DateTime? @db.Timestamp(6) - attempt_count BigInt - id_webhooks_payload String? @db.Uuid - id_webhook_endpoint String? @db.Uuid - id_event String? @db.Uuid - id_webhooks_reponse String? @db.Uuid - webhooks_payloads webhooks_payloads? @relation(fields: [id_webhooks_payload], references: [id_webhooks_payload], onDelete: NoAction, onUpdate: NoAction, map: "fk_38_1") - webhook_endpoints webhook_endpoints? @relation(fields: [id_webhook_endpoint], references: [id_webhook_endpoint], onDelete: NoAction, onUpdate: NoAction, map: "fk_38_2") - events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_39") - webhooks_reponses webhooks_reponses? @relation(fields: [id_webhooks_reponse], references: [id_webhooks_reponse], onDelete: NoAction, onUpdate: NoAction, map: "fk_40") +model acc_purchase_orders_line_items { + id_acc_purchase_orders_line_item String @id(map: "pk_acc_purchase_orders_line_items") @db.Uuid + id_acc_purchase_order String @db.Uuid + remote_id String? + modified_at DateTime @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + description String? + unit_price BigInt? + quantity BigInt? + tracking_categories String[] + tax_amount BigInt? + total_line_amount BigInt? + currency String? + exchange_rate String? + id_acc_account String? @db.Uuid + id_acc_company String? @db.Uuid - @@index([id_webhooks_payload], map: "fk_we_payload_webhookid") - @@index([id_webhook_endpoint], map: "fk_we_webhookendpointid") - @@index([id_event], map: "fk_webhook_delivery_attempt_eventid") - @@index([id_webhooks_reponse], map: "fk_webhook_delivery_attempt_webhook_responseid") + @@index([id_acc_purchase_order], map: "fk_purchaseorder_purchaseorderlineitems") } -model connector_sets { - id_connector_set String @id(map: "pk_project_connector") @db.Uuid - crm_hubspot Boolean? - crm_zoho Boolean? - crm_attio Boolean? - crm_pipedrive Boolean? - tcg_zendesk Boolean? - tcg_jira Boolean? - tcg_gorgias Boolean? - tcg_gitlab Boolean? - tcg_front Boolean? - crm_zendesk Boolean? - crm_close Boolean? - fs_box Boolean? - tcg_github Boolean? - ecom_woocommerce Boolean? - ecom_shopify Boolean? - projects projects[] +model acc_report_items { + id_acc_report_item String @id(map: "pk_acc_report_items") @db.Uuid + name String? + value BigInt? + company String? @db.Uuid + parent_item String? @db.Uuid + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model managed_webhooks { - id_managed_webhook String @id(map: "pk_managed_webhooks") @db.Uuid - active Boolean - id_connection String @db.Uuid - endpoint String @db.Uuid - api_version String? - active_events String[] - remote_signing_secret String? - modified_at DateTime @db.Timestamp(6) - created_at DateTime @db.Timestamp(6) +model acc_tax_rates { + id_acc_tax_rate String @id(map: "pk_acc_tax_rates") @db.Uuid + remote_id String? + description String? + total_tax_ratge BigInt? + effective_tax_rate BigInt? + company String? @db.Uuid + id_connection String @db.Uuid + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) } -model fs_drives { - id_fs_drive String @id(map: "pk_fs_drives") @db.Uuid - drive_url String? - name String? - remote_created_at DateTime? @db.Timestamp(6) - remote_id String? - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_connection String @db.Uuid +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model acc_tracking_categories { + id_acc_tracking_category String @id(map: "pk_acc_tracking_categories") @db.Uuid + remote_id String? + name String? + status String? + category_type String? + parent_category String? @db.Uuid + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid } -model fs_files { - id_fs_file String @id(map: "pk_fs_files") @db.Uuid - name String? - file_url String? - mime_type String? - size BigInt? - remote_id String? - id_fs_permission String? @db.Uuid - id_fs_folder String? @db.Uuid - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_connection String @db.Uuid - - @@index([id_fs_folder], map: "fk_fs_file_folderid") - @@index([id_fs_permission], map: "fk_fs_file_permissionid") +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model acc_transactions { + id_acc_transaction String @id(map: "pk_acc_transactions") @db.Uuid + transaction_type String? + number BigInt? + transaction_date DateTime? @db.Timestamptz(6) + total_amount String? + exchange_rate String? + currency String? + tracking_categories String[] + id_acc_account String? @db.Uuid + id_acc_contact String? @db.Uuid + id_acc_company_info String? @db.Uuid + id_acc_accounting_period String? @db.Uuid + remote_id String + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid } -model fs_folders { - id_fs_folder String @id(map: "pk_fs_folders") @db.Uuid - folder_url String? - size BigInt? - name String? - description String? - parent_folder String? @db.Uuid - remote_id String? - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_fs_drive String? @db.Uuid - id_connection String @db.Uuid - id_fs_permission String? @db.Uuid +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model acc_transactions_lines_items { + id_acc_transactions_lines_item String @id(map: "pk_acc_transactions_lines_items") @db.Uuid + memo String? + unit_price String? + quantity String? + total_line_amount String? + id_acc_tax_rate String? @db.Uuid + currency String? + exchange_rate String? + tracking_categories String[] + id_acc_company_info String? @db.Uuid + id_acc_item String? @db.Uuid + id_acc_account String? @db.Uuid + remote_id String? + created_at DateTime @db.Timetz(6) + modified_at DateTime @db.Timetz(6) + id_acc_transaction String? @db.Uuid - @@index([id_fs_drive], map: "fk_fs_folder_driveid") - @@index([id_fs_permission], map: "fk_fs_folder_permissionid") + @@index([id_acc_transaction], map: "fk_acc_transactions_lineitems") +} + +model acc_vendor_credit_lines { + id_acc_vendor_credit_line String @id(map: "pk_acc_vendor_credit_lines") @db.Uuid + net_amount BigInt? + tracking_categories String[] + description String? + account String? @db.Uuid + id_acc_account String? @db.Uuid + exchange_rate String? + id_acc_company_info String? @db.Uuid + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_acc_vendor_credit String? @db.Uuid } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model fs_permissions { - id_fs_permission String @id(map: "pk_fs_permissions") @db.Uuid - remote_id String? - user String? @db.Uuid - group String? @db.Uuid - type String? - roles String[] - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_connection String @db.Uuid +model acc_vendor_credits { + id_acc_vendor_credit String @id(map: "pk_acc_vendor_credits") @db.Uuid + id_connection String @db.Uuid + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + number String? + transaction_date DateTime? @db.Timestamptz(6) + vendor String? @db.Uuid + total_amount BigInt? + currency String? + exchange_rate String? + company String? @db.Uuid + tracking_categories String[] + accounting_period String? @db.Uuid } -model fs_shared_links { - id_fs_shared_link String @id(map: "pk_fs_shared_links") @db.Uuid - url String? - download_url String? - scope String? - password_protected Boolean - password String? - expires_at DateTime? @db.Timestamptz(6) - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid - id_fs_folder String? @db.Uuid - id_fs_file String? @db.Uuid - remote_id String? +model api_keys { + id_api_key String @id(map: "id_") @db.Uuid + api_key_hash String @unique(map: "unique_api_keys") + name String? + id_project String @db.Uuid + id_user String @db.Uuid + projects projects @relation(fields: [id_project], references: [id_project], onDelete: NoAction, onUpdate: NoAction, map: "fk_api_key_project_id") + users users @relation(fields: [id_user], references: [id_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_api_keys_user_id") + + @@index([id_project], map: "fk_api_keys_projects") + @@index([id_user], map: "fkx_api_keys_user_id") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments @@ -805,964 +632,1492 @@ model ats_applications { created_at DateTime @db.Timestamptz(6) modified_at DateTime @db.Timestamptz(6) id_connection String @db.Uuid - - @@index([id_ats_job], map: "fk_ats_application_ats_job_id") - @@index([id_ats_candidate], map: "fk_ats_application_atscandidateid") + + @@index([id_ats_job], map: "fk_ats_application_ats_job_id") + @@index([id_ats_candidate], map: "fk_ats_application_atscandidateid") +} + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model ats_candidate_attachments { + id_ats_candidate_attachment String @id(map: "pk_ats_candidate_attachments") @db.Uuid + remote_id String? + file_url String? + file_name String? + remote_created_at DateTime? @db.Timestamptz(6) + remote_modified_at DateTime? @db.Timestamptz(6) + file_type String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_ats_candidate String @db.Uuid + id_connection String @db.Uuid + + @@index([id_ats_candidate], map: "fk_ats_candidate_attachment_candidateid_index") +} + +model ats_candidate_email_addresses { + id_ats_candidate_email_address String @id(map: "pk_ats_candidate_email_addresses") @db.Uuid + value String? + type String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_ats_candidate String @db.Uuid + + @@index([id_ats_candidate], map: "fk_candidate_email_id") +} + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model ats_candidate_phone_numbers { + id_ats_candidate_phone_number String @id(map: "pk_ats_candidate_phone_numbers") @db.Uuid + value String? + type String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_ats_candidate String @db.Uuid + + @@index([id_ats_candidate], map: "fk_candidate_phone_id") +} + +model ats_candidate_tags { + id_ats_candidate_tag String @id(map: "pk_ats_candidate_tags") @db.Uuid + name String? + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid +} + +model ats_candidate_urls { + id_ats_candidate_url String @id(map: "pk_ats_candidate_urls") @db.Uuid + value String? + type String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_ats_candidate String @db.Uuid + + @@index([id_ats_candidate], map: "fk_candidate_url_id") +} + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model ats_candidates { + id_ats_candidate String @id(map: "pk_ats_candidates") @db.Uuid + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + first_name String? + last_name String? + company String? + title String? + remote_created_at DateTime? @db.Timestamptz(6) + remote_modified_at DateTime? @db.Timestamptz(6) + last_interaction_at DateTime? @db.Timestamptz(6) + is_private Boolean? + email_reachable Boolean? + locations String? + tags String[] + id_connection String @db.Uuid +} + +model ats_departments { + id_ats_department String @id(map: "pk_ats_departments") @db.Uuid + name String? + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid +} + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model ats_eeocs { + id_ats_eeoc String @id(map: "pk_ats_eeocs") @db.Uuid + id_ats_candidate String? @db.Uuid + submitted_at DateTime? @db.Timestamptz(6) + race String? + gender String? + veteran_status String? + disability_status String? + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid + + @@index([id_ats_candidate], map: "fk_candidate_eeocsid") +} + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model ats_interviews { + id_ats_interview String @id(map: "pk_ats_interviews") @db.Uuid + status String? + organized_by String? @db.Uuid + interviewers String[] + location String? + start_at DateTime? @db.Timestamptz(6) + end_at DateTime? @db.Timestamptz(6) + remote_created_at DateTime? @db.Timestamptz(6) + remote_updated_at DateTime? @db.Timestamptz(6) + remote_id String? + id_ats_application String? @db.Uuid + id_ats_job_interview_stage String? @db.Uuid + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid + + @@index([id_ats_application], map: "fk_applications_interviews") + @@index([id_ats_job_interview_stage], map: "fk_id_ats_job_interview_stageid") +} + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model ats_job_interview_stages { + id_ats_job_interview_stage String @id(map: "pk_ats_job_interview_stages") @db.Uuid + name String? + stage_order Int? + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_ats_job String? @db.Uuid + id_connection String @db.Uuid + + @@index([id_ats_job], map: "fk_ats_jobs_ats_jobinterview_id") +} + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model ats_jobs { + id_ats_job String @id(map: "pk_ats_jobs") @db.Uuid + name String? + description String? + code String? + status String? + type String? + confidential Boolean? + ats_departments String[] + ats_offices String[] + managers String[] + recruiters String[] + remote_id String? + remote_created_at DateTime? @db.Timestamptz(6) + remote_updated_at DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid +} + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model ats_offers { + id_ats_offer String @id(map: "pk_ats_offers") @db.Uuid + remote_id String? + created_by String? @db.Uuid + remote_created_at DateTime? @db.Timestamptz(6) + closed_at DateTime? @db.Timestamptz(6) + sent_at DateTime? @db.Timestamptz(6) + start_date DateTime? @db.Timestamptz(6) + status String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_ats_application String @db.Uuid + id_connection String @db.Uuid + + @@index([id_ats_application], map: "fk_ats_offers_applicationid") +} + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model ats_offices { + id_ats_office String @id(map: "pk_ats_offices") @db.Uuid + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + name String? + location String? + id_connection String @db.Uuid +} + +model ats_reject_reasons { + id_ats_reject_reason String @id(map: "pk_ats_reject_reasons") @db.Uuid + name String? + remote_id String? + modified_at DateTime @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid +} + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model ats_scorecards { + id_ats_scorecard String @id(map: "pk_ats_scorecards") @db.Uuid + overall_recommendation String? + id_ats_application String? @db.Uuid + id_ats_interview String? @db.Uuid + remote_id String? + remote_created_at DateTime? @db.Timestamptz(6) + submitted_at DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid + + @@index([id_ats_application], map: "fk_applications_scorecard") + @@index([id_ats_interview], map: "fk_interviews_scorecards") +} + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model ats_users { + id_ats_user String @id(map: "pk_ats_users") @db.Uuid + remote_id String? + first_name String? + last_name String? + email String? + disabled Boolean? + access_role String? + remote_created_at DateTime? @db.Timestamp(6) + remote_modified_at DateTime? @db.Timestamp(6) + created_at DateTime @db.Timestamp(6) + modified_at DateTime @db.Timestamp(6) + id_connection String @db.Uuid } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model ats_candidate_attachments { - id_ats_candidate_attachment String @id(map: "pk_ats_candidate_attachments") @db.Uuid - remote_id String? - file_url String? - file_name String? - remote_created_at DateTime? @db.Timestamptz(6) - remote_modified_at DateTime? @db.Timestamptz(6) - file_type String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_ats_candidate String @db.Uuid - id_connection String @db.Uuid +model attribute { + id_attribute String @id(map: "pk_attribute") @db.Uuid + status String + ressource_owner_type String + slug String + description String + data_type String + remote_id String + source String + id_entity String? @db.Uuid + id_project String @db.Uuid + scope String + id_consumer String? @db.Uuid + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + entity entity? @relation(fields: [id_entity], references: [id_entity], onDelete: NoAction, onUpdate: NoAction, map: "fk_32") + value value[] - @@index([id_ats_candidate], map: "fk_ats_candidate_attachment_candidateid_index") + @@index([id_entity], map: "fk_attribute_entityid") } -model ats_candidate_email_addresses { - id_ats_candidate_email_address String @id(map: "pk_ats_candidate_email_addresses") @db.Uuid - value String? - type String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_ats_candidate String @db.Uuid - - @@index([id_ats_candidate], map: "fk_candidate_email_id") +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model connection_strategies { + id_connection_strategy String @id(map: "pk_connection_strategies") @db.Uuid + status Boolean + type String + id_project String? @db.Uuid } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model ats_candidate_phone_numbers { - id_ats_candidate_phone_number String @id(map: "pk_ats_candidate_phone_numbers") @db.Uuid - value String? - type String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_ats_candidate String @db.Uuid +model connections { + id_connection String @id(map: "pk_connections") @db.Uuid + status String + provider_slug String + vertical String + account_url String? + token_type String + access_token String? + refresh_token String? + expiration_timestamp DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + connection_token String? + id_project String @db.Uuid + id_linked_user String @db.Uuid + linked_users linked_users @relation(fields: [id_linked_user], references: [id_linked_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_11") + projects projects @relation(fields: [id_project], references: [id_project], onDelete: NoAction, onUpdate: NoAction, map: "fk_9") - @@index([id_ats_candidate], map: "fk_candidate_phone_id") + @@index([id_project], map: "fk_1") + @@index([id_linked_user], map: "fk_connections_to_linkedusersid") } -model ats_candidate_tags { - id_ats_candidate_tag String @id(map: "pk_ats_candidate_tags") @db.Uuid - name String? - remote_id String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid +model connector_sets { + id_connector_set String @id(map: "pk_project_connector") @db.Uuid + crm_hubspot Boolean? + crm_zoho Boolean? + crm_attio Boolean? + crm_pipedrive Boolean? + tcg_zendesk Boolean? + tcg_jira Boolean? + tcg_gorgias Boolean? + tcg_gitlab Boolean? + tcg_front Boolean? + crm_zendesk Boolean? + crm_close Boolean? + fs_box Boolean? + tcg_github Boolean? + ecom_woocommerce Boolean? + ecom_shopify Boolean? + ecom_amazon Boolean? + ecom_squarespace Boolean? + projects projects[] } -model ats_candidate_urls { - id_ats_candidate_url String @id(map: "pk_ats_candidate_urls") @db.Uuid - value String? - type String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_ats_candidate String @db.Uuid +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model crm_addresses { + id_crm_address String @id(map: "pk_crm_addresses") @db.Uuid + street_1 String? + street_2 String? + city String? + state String? + postal_code String? + country String? + address_type String? + id_crm_company String? @db.Uuid + id_crm_contact String? @db.Uuid + id_connection String @db.Uuid + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + owner_type String + crm_contacts crm_contacts? @relation(fields: [id_crm_contact], references: [id_crm_contact], onDelete: NoAction, onUpdate: NoAction, map: "fk_14") + crm_companies crm_companies? @relation(fields: [id_crm_company], references: [id_crm_company], onDelete: NoAction, onUpdate: NoAction, map: "fk_15") - @@index([id_ats_candidate], map: "fk_candidate_url_id") + @@index([id_crm_contact], map: "fk_crm_addresses_to_crm_contacts") + @@index([id_crm_company], map: "fk_crm_adresses_to_crm_companies") } -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model ats_candidates { - id_ats_candidate String @id(map: "pk_ats_candidates") @db.Uuid +model crm_companies { + id_crm_company String @id(map: "pk_crm_companies") @db.Uuid + name String? + industry String? + number_of_employees BigInt? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) remote_id String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - first_name String? - last_name String? - company String? - title String? - remote_created_at DateTime? @db.Timestamptz(6) - remote_modified_at DateTime? @db.Timestamptz(6) - last_interaction_at DateTime? @db.Timestamptz(6) - is_private Boolean? - email_reachable Boolean? - locations String? - tags String[] - id_connection String @db.Uuid -} + remote_platform String? + id_crm_user String? @db.Uuid + id_linked_user String? @db.Uuid + id_connection String @db.Uuid + crm_addresses crm_addresses[] + crm_users crm_users? @relation(fields: [id_crm_user], references: [id_crm_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_24") + crm_deals crm_deals[] + crm_email_addresses crm_email_addresses[] + crm_engagements crm_engagements[] + crm_notes crm_notes[] + crm_phone_numbers crm_phone_numbers[] + crm_tasks crm_tasks[] -model ats_departments { - id_ats_department String @id(map: "pk_ats_departments") @db.Uuid - name String? - remote_id String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid + @@index([id_crm_user], map: "fk_crm_company_crm_userid") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model ats_eeocs { - id_ats_eeoc String @id(map: "pk_ats_eeocs") @db.Uuid - id_ats_candidate String? @db.Uuid - submitted_at DateTime? @db.Timestamptz(6) - race String? - gender String? - veteran_status String? - disability_status String? - remote_id String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid +model crm_contacts { + id_crm_contact String @id(map: "pk_crm_contacts") @db.Uuid + first_name String? + last_name String? + created_at DateTime? @db.Timestamptz(6) + modified_at DateTime? @db.Timestamptz(6) + remote_id String? + remote_platform String? + id_crm_user String? @db.Uuid + id_linked_user String? @db.Uuid + id_connection String @db.Uuid + crm_addresses crm_addresses[] + crm_users crm_users? @relation(fields: [id_crm_user], references: [id_crm_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_23") + crm_email_addresses crm_email_addresses[] + crm_notes crm_notes[] + crm_phone_numbers crm_phone_numbers[] - @@index([id_ats_candidate], map: "fk_candidate_eeocsid") + @@index([id_crm_user], map: "fk_crm_contact_userid") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model ats_interviews { - id_ats_interview String @id(map: "pk_ats_interviews") @db.Uuid - status String? - organized_by String? @db.Uuid - interviewers String[] - location String? - start_at DateTime? @db.Timestamptz(6) - end_at DateTime? @db.Timestamptz(6) - remote_created_at DateTime? @db.Timestamptz(6) - remote_updated_at DateTime? @db.Timestamptz(6) - remote_id String? - id_ats_application String? @db.Uuid - id_ats_job_interview_stage String? @db.Uuid - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid +model crm_deals { + id_crm_deal String @id(map: "pk_crm_deal") @db.Uuid + name String + description String? + amount BigInt + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + remote_id String? + remote_platform String? + id_crm_user String? @db.Uuid + id_crm_deals_stage String? @db.Uuid + id_linked_user String? @db.Uuid + id_crm_company String? @db.Uuid + id_connection String @db.Uuid + crm_deals_stages crm_deals_stages? @relation(fields: [id_crm_deals_stage], references: [id_crm_deals_stage], onDelete: NoAction, onUpdate: NoAction, map: "fk_21") + crm_users crm_users? @relation(fields: [id_crm_user], references: [id_crm_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_22") + crm_companies crm_companies? @relation(fields: [id_crm_company], references: [id_crm_company], onDelete: NoAction, onUpdate: NoAction, map: "fk_47_1") + crm_notes crm_notes[] + crm_tasks crm_tasks[] - @@index([id_ats_application], map: "fk_applications_interviews") - @@index([id_ats_job_interview_stage], map: "fk_id_ats_job_interview_stageid") + @@index([id_crm_user], map: "crm_deal_crm_userid") + @@index([id_crm_deals_stage], map: "crm_deal_deal_stageid") + @@index([id_crm_company], map: "fk_crm_deal_crmcompanyid") } -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model ats_job_interview_stages { - id_ats_job_interview_stage String @id(map: "pk_ats_job_interview_stages") @db.Uuid - name String? - stage_order Int? - remote_id String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_ats_job String? @db.Uuid - id_connection String @db.Uuid +model crm_deals_stages { + id_crm_deals_stage String @id(map: "pk_crm_deal_stages") @db.Uuid + stage_name String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_linked_user String? @db.Uuid + remote_id String? + remote_platform String? + id_connection String @db.Uuid + crm_deals crm_deals[] +} - @@index([id_ats_job], map: "fk_ats_jobs_ats_jobinterview_id") +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model crm_email_addresses { + id_crm_email String @id(map: "pk_crm_contact_email_addresses") @db.Uuid + email_address String + email_address_type String + owner_type String + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_crm_company String? @db.Uuid + id_crm_contact String? @db.Uuid + id_connection String @db.Uuid + crm_companies crm_companies? @relation(fields: [id_crm_company], references: [id_crm_company], onDelete: NoAction, onUpdate: NoAction, map: "fk_16") + crm_contacts crm_contacts? @relation(fields: [id_crm_contact], references: [id_crm_contact], onDelete: NoAction, onUpdate: NoAction, map: "fk_3") + + @@index([id_crm_contact], map: "crm_contactid_crm_contact_email_address") + @@index([id_crm_company], map: "fk_contact_email_adress_companyid") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model ats_jobs { - id_ats_job String @id(map: "pk_ats_jobs") @db.Uuid - name String? - description String? - code String? - status String? +model crm_engagements { + id_crm_engagement String @id(map: "pk_crm_engagement") @db.Uuid + content String? type String? - confidential Boolean? - ats_departments String[] - ats_offices String[] - managers String[] - recruiters String[] + direction String? + subject String? + start_at DateTime? @db.Timestamptz(6) + end_time DateTime? @db.Timestamptz(6) remote_id String? - remote_created_at DateTime? @db.Timestamptz(6) - remote_updated_at DateTime? @db.Timestamptz(6) - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid + id_linked_user String? @db.Uuid + remote_platform String? + id_crm_company String? @db.Uuid + id_crm_user String? @db.Uuid + id_connection String @db.Uuid + contacts String[] + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + crm_companies crm_companies? @relation(fields: [id_crm_company], references: [id_crm_company], onDelete: NoAction, onUpdate: NoAction, map: "fk_29") + crm_users crm_users? @relation(fields: [id_crm_user], references: [id_crm_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_crm_engagement_crm_user") + + @@index([id_crm_user], map: "fk_crm_engagement_crm_user_id") + @@index([id_crm_company], map: "fk_crm_engagement_crmcompanyid") } -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model ats_offers { - id_ats_offer String @id(map: "pk_ats_offers") @db.Uuid - remote_id String? - created_by String? @db.Uuid - remote_created_at DateTime? @db.Timestamptz(6) - closed_at DateTime? @db.Timestamptz(6) - sent_at DateTime? @db.Timestamptz(6) - start_date DateTime? @db.Timestamptz(6) - status String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_ats_application String @db.Uuid - id_connection String @db.Uuid +model crm_notes { + id_crm_note String @id(map: "pk_crm_notes") @db.Uuid + content String + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_crm_company String? @db.Uuid + id_crm_contact String? @db.Uuid + id_crm_deal String? @db.Uuid + id_linked_user String? @db.Uuid + remote_id String? + remote_platform String? + id_crm_user String? @db.Uuid + id_connection String @db.Uuid + crm_companies crm_companies? @relation(fields: [id_crm_company], references: [id_crm_company], onDelete: NoAction, onUpdate: NoAction, map: "fk_18") + crm_contacts crm_contacts? @relation(fields: [id_crm_contact], references: [id_crm_contact], onDelete: NoAction, onUpdate: NoAction, map: "fk_19") + crm_deals crm_deals? @relation(fields: [id_crm_deal], references: [id_crm_deal], onDelete: NoAction, onUpdate: NoAction, map: "fk_20") - @@index([id_ats_application], map: "fk_ats_offers_applicationid") + @@index([id_crm_contact], map: "fk_crm_note_crm_companyid") + @@index([id_crm_company], map: "fk_crm_note_crm_contactid") + @@index([id_crm_user], map: "fk_crm_note_crm_userid") + @@index([id_crm_deal], map: "fk_crm_notes_crm_dealid") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model ats_offices { - id_ats_office String @id(map: "pk_ats_offices") @db.Uuid - remote_id String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - name String? - location String? - id_connection String @db.Uuid -} +model crm_phone_numbers { + id_crm_phone_number String @id(map: "pk_crm_contacts_phone_numbers") @db.Uuid + phone_number String? + phone_type String? + owner_type String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_crm_company String? @db.Uuid + id_crm_contact String? @db.Uuid + id_connection String @db.Uuid + crm_companies crm_companies? @relation(fields: [id_crm_company], references: [id_crm_company], onDelete: NoAction, onUpdate: NoAction, map: "fk_17") + crm_contacts crm_contacts? @relation(fields: [id_crm_contact], references: [id_crm_contact], onDelete: NoAction, onUpdate: NoAction, map: "fk_phonenumber_crm_contactid") -model ats_reject_reasons { - id_ats_reject_reason String @id(map: "pk_ats_reject_reasons") @db.Uuid - name String? - remote_id String? - modified_at DateTime @db.Timestamptz(6) - created_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid + @@index([id_crm_contact], map: "crm_contactid_crm_contact_phone_number") + @@index([id_crm_company], map: "fk_phone_number_companyid") } -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model ats_scorecards { - id_ats_scorecard String @id(map: "pk_ats_scorecards") @db.Uuid - overall_recommendation String? - id_ats_application String? @db.Uuid - id_ats_interview String? @db.Uuid - remote_id String? - remote_created_at DateTime? @db.Timestamptz(6) - submitted_at DateTime? @db.Timestamptz(6) - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid +model crm_tasks { + id_crm_task String @id(map: "pk_crm_task") @db.Uuid + subject String? + content String? + status String? + due_date DateTime? @db.Timestamptz(6) + finished_date DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_crm_user String? @db.Uuid + id_crm_company String? @db.Uuid + id_crm_deal String? @db.Uuid + id_linked_user String? @db.Uuid + remote_id String? + remote_platform String? + id_connection String @db.Uuid + crm_users crm_users? @relation(fields: [id_crm_user], references: [id_crm_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_25") + crm_companies crm_companies? @relation(fields: [id_crm_company], references: [id_crm_company], onDelete: NoAction, onUpdate: NoAction, map: "fk_26") + crm_deals crm_deals? @relation(fields: [id_crm_deal], references: [id_crm_deal], onDelete: NoAction, onUpdate: NoAction, map: "fk_27") - @@index([id_ats_application], map: "fk_applications_scorecard") - @@index([id_ats_interview], map: "fk_interviews_scorecards") + @@index([id_crm_company], map: "fk_crm_task_companyid") + @@index([id_crm_user], map: "fk_crm_task_userid") + @@index([id_crm_deal], map: "fk_crmtask_dealid") } -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model ats_users { - id_ats_user String @id(map: "pk_ats_users") @db.Uuid - remote_id String? - first_name String? - last_name String? - email String? - disabled Boolean? - access_role String? - remote_created_at DateTime? @db.Timestamp(6) - remote_modified_at DateTime? @db.Timestamp(6) - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_connection String @db.Uuid +model crm_users { + id_crm_user String @id(map: "pk_crm_users") @db.Uuid + name String? + email String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_linked_user String? @db.Uuid + remote_id String? + remote_platform String? + id_connection String @db.Uuid + crm_companies crm_companies[] + crm_contacts crm_contacts[] + crm_deals crm_deals[] + crm_engagements crm_engagements[] + crm_tasks crm_tasks[] } -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model fs_groups { - id_fs_group String @id(map: "pk_fs_groups") @db.Uuid - name String? - users String[] - remote_id String? - remote_was_deleted Boolean - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_connection String @db.Uuid +model cs_attributes { + id_cs_attribute String @id(map: "pk_ct_attributes") @db.Uuid + attribute_slug String + data_type String + id_cs_entity String @db.Uuid } -model fs_users { - id_fs_user String @id(map: "pk_fs_users") @db.Uuid - name String? - email String? - is_me Boolean - remote_id String? - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) - id_connection String @db.Uuid +model cs_entities { + id_cs_entity String @id(map: "pk_ct_entities") @db.Uuid + id_connection_strategy String @db.Uuid } -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_accounting_periods { - id_acc_accounting_period String @id(map: "pk_acc_accounting_periods") @db.Uuid - remote_id String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - name String? - status String? - start_date DateTime? @db.Timestamptz(6) - end_date DateTime? @db.Timestamptz(6) - id_connection String @db.Uuid +model cs_values { + id_cs_value String @id(map: "pk_ct_values") @db.Uuid + value String + id_cs_attribute String @db.Uuid } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_accounts { - id_acc_account String @id(map: "pk_acc_accounts") @db.Uuid - name String? - description String? - classification String? - type String? - status String? - current_balance BigInt? - currency String? - account_number String? - parent_account String? @db.Uuid - remote_id String? - id_acc_company_info String? @db.Uuid - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid +model ecom_addresses { + id_ecom_address String @id(map: "pk_ecom_customer_addresses") @db.Uuid + address_type String? + street_1 String? + street_2 String? + city String? + state String? + postal_code String? + country String? + id_ecom_customer String @db.Uuid + modified_at DateTime @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + remote_deleted Boolean + id_ecom_order String @db.Uuid + ecom_customers ecom_customers @relation(fields: [id_ecom_customer], references: [id_ecom_customer], onDelete: NoAction, onUpdate: NoAction, map: "fk_ecom_customer_customeraddress") + ecom_orders ecom_orders @relation(fields: [id_ecom_order], references: [id_ecom_order], onDelete: NoAction, onUpdate: NoAction, map: "fk_ecom_order_address") + + @@index([id_ecom_customer], map: "fk_index_ecom_customer_customeraddress") + @@index([id_ecom_order], map: "fk_index_fk_ecom_order_address") +} + +model ecom_customers { + id_ecom_customer String @id(map: "pk_ecom_customers") @db.Uuid + remote_id String? + email String? + first_name String? + last_name String? + phone_number String? + modified_at DateTime @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid + remote_deleted Boolean + ecom_addresses ecom_addresses[] + ecom_orders ecom_orders[] +} - @@index([id_acc_company_info], map: "fk_accounts_companyinfo_id") +model ecom_fulfilment_orders { + id_ecom_fulfilment_order String @id(map: "pk_ecom_fulfilment_order") @db.Uuid } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_addresses { - id_acc_address String @id(map: "pk_acc_addresses") @db.Uuid - type String? - street_1 String? - street_2 String? - city String? - state String? - country_subdivision String? - country String? - zip String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_acc_contact String? @db.Uuid - id_acc_company_info String? @db.Uuid - id_connection String @db.Uuid +model ecom_fulfilments { + id_ecom_fulfilment String @id(map: "pk_ecom_fulfilments") @db.Uuid + carrier String? + tracking_urls String[] + tracking_numbers String[] + items Json? + remote_id String? + id_ecom_order String? @db.Uuid + id_connection String @db.Uuid + modified_at DateTime @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + remote_deleted Boolean + ecom_orders ecom_orders? @relation(fields: [id_ecom_order], references: [id_ecom_order], onDelete: NoAction, onUpdate: NoAction, map: "fk_ecom_order_fulfilment") - @@index([id_acc_company_info], map: "fk_acc_company_info_acc_adresses") - @@index([id_acc_contact], map: "fk_acc_contact_acc_addresses") + @@index([id_ecom_order], map: "fk_index_ecom_order_fulfilment") } -model acc_attachments { - id_acc_attachment String @id(map: "pk_acc_attachments") @db.Uuid - file_name String? - file_url String? - remote_id String? - id_acc_account String? @db.Uuid - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid +model ecom_order_line_items { + id_ecom_order_line_item String @id(map: "pk_106") @db.Uuid +} - @@index([id_acc_account], map: "fk_acc_attachments_accountid") +model ecom_orders { + id_ecom_order String @id(map: "pk_ecom_orders") @db.Uuid + order_status String? + order_number String? + payment_status String? + currency String? + total_price BigInt? + total_discount BigInt? + total_shipping BigInt? + total_tax BigInt? + fulfillment_status String? + remote_id String? + id_ecom_customer String? @db.Uuid + id_connection String @db.Uuid + modified_at DateTime @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + remote_deleted Boolean + ecom_addresses ecom_addresses[] + ecom_fulfilments ecom_fulfilments[] + ecom_customers ecom_customers? @relation(fields: [id_ecom_customer], references: [id_ecom_customer], onDelete: NoAction, onUpdate: NoAction, map: "fk_ecom_customer_orders") + + @@index([id_ecom_customer], map: "fk_index_ecom_customer_orders") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_balance_sheets { - id_acc_balance_sheet String @id(map: "pk_acc_balance_sheets") @db.Uuid - name String? - currency String? - id_acc_company_info String? @db.Uuid - date DateTime? @db.Timestamptz(6) - net_assets BigInt? - assets String[] - liabilities String[] - equity String[] - remote_generated_at DateTime? @db.Timestamptz(6) - remote_id String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid +model ecom_product_variants { + id_ecom_product_variant String @id(map: "pk_ecom_product_variants") @db.Uuid + id_connection String @db.Uuid + remote_id String? + title String? + price BigInt? + sku String? + options Json? + weight BigInt? + inventory_quantity BigInt? + id_ecom_product String? @db.Uuid + modified_at DateTime @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + remote_deleted Boolean + ecom_products ecom_products? @relation(fields: [id_ecom_product], references: [id_ecom_product], onDelete: NoAction, onUpdate: NoAction, map: "fk_ecom_products_variants") - @@index([id_acc_company_info], map: "fk_balancesheetcompanyinfoid") + @@index([id_ecom_product], map: "fk_index_ecom_products_variants") +} + +model ecom_products { + id_ecom_product String @id(map: "pk_ecom_products") @db.Uuid + remote_id String? + product_url String? + product_type String? + product_status String? + images_urls String[] + description String? + vendor String? + tags String[] + modified_at DateTime @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid + remote_deleted Boolean + ecom_product_variants ecom_product_variants[] } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_balance_sheets_report_items { - id_acc_balance_sheets_report_item String @id(map: "pk_acc_balance_sheets_report_items") @db.Uuid - remote_id String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - name String? - value BigInt? - parent_item String? @db.Uuid - id_acc_company_info String? @db.Uuid +model entity { + id_entity String @id(map: "pk_entity") @db.Uuid + ressource_owner_id String @db.Uuid + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + attribute attribute[] + value value[] } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_cash_flow_statement_report_items { - id_acc_cash_flow_statement_report_item String @id(map: "pk_acc_cash_flow_statement_report_items") @db.Uuid - name String? - value BigInt? - type String? - parent_item String? @db.Uuid - remote_generated_at DateTime? @db.Timestamptz(6) - remote_id String? - modified_at DateTime @db.Timestamptz(6) - created_at DateTime @db.Timestamptz(6) - id_acc_cash_flow_statement String? @db.Uuid +model events { + id_event String @id(map: "pk_jobs") @db.Uuid + id_connection String @db.Uuid + id_project String @db.Uuid + type String + status String + direction String + method String + url String + provider String + timestamp DateTime @default(now()) @db.Timestamptz(6) + id_linked_user String @db.Uuid + linked_users linked_users @relation(fields: [id_linked_user], references: [id_linked_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_12") + jobs_status_history jobs_status_history[] + webhook_delivery_attempts webhook_delivery_attempts[] - @@index([id_acc_cash_flow_statement], map: "fk_cashflow_statement_acc_cash_flow_statement_report_item") + @@index([id_linked_user], map: "fk_linkeduserid_projectid") } -model acc_cash_flow_statements { - id_acc_cash_flow_statement String @id(map: "pk_acc_cash_flow_statements") @db.Uuid - name String? - currency String? - company String? @db.Uuid - start_period DateTime? @db.Timestamptz(6) - end_period DateTime? @db.Timestamptz(6) - cash_at_beginning_of_period BigInt? - cash_at_end_of_period BigInt? - remote_generated_at DateTime? @db.Timestamptz(6) - remote_id String? - modified_at DateTime @db.Timestamptz(6) - created_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid +model fs_drives { + id_fs_drive String @id(map: "pk_fs_drives") @db.Uuid + drive_url String? + name String? + remote_created_at DateTime? @db.Timestamptz(6) + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid } -model acc_company_infos { - id_acc_company_info String @id(map: "pk_acc_company_infos") @db.Uuid - name String? - legal_name String? - tax_number String? - fiscal_year_end_month Int? - fiscal_year_end_day Int? - currency String? - remote_created_at DateTime? @db.Timestamptz(6) - remote_id String? - urls String[] - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid - tracking_categories String[] +model fs_files { + id_fs_file String @id(map: "pk_fs_files") @db.Uuid + name String? + file_url String? + mime_type String? + size BigInt? + remote_id String? + id_fs_permission String? @db.Uuid + id_fs_folder String? @db.Uuid + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid + + @@index([id_fs_folder], map: "fk_fs_file_folderid") + @@index([id_fs_permission], map: "fk_fs_file_permissionid") } -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_contacts { - id_acc_contact String @id(map: "pk_acc_contacts") @db.Uuid - name String? - is_supplier Boolean? - is_customer Boolean? - email_address String? - tax_number String? - status String? - currency String? - remote_updated_at String? - id_acc_company_info String? @db.Uuid - id_connection String @db.Uuid - remote_id String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) +model fs_folders { + id_fs_folder String @id(map: "pk_fs_folders") @db.Uuid + folder_url String? + size BigInt? + name String? + description String? + parent_folder String? @db.Uuid + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_fs_drive String? @db.Uuid + id_connection String @db.Uuid + id_fs_permission String? @db.Uuid - @@index([id_acc_company_info], map: "fk_acc_contact_company") + @@index([id_fs_drive], map: "fk_fs_folder_driveid") + @@index([id_fs_permission], map: "fk_fs_folder_permissionid") +} + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model fs_groups { + id_fs_group String @id(map: "pk_fs_groups") @db.Uuid + name String? + users String[] + remote_id String? + remote_was_deleted Boolean + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_credit_notes { - id_acc_credit_note String @id(map: "pk_acc_credit_notes") @db.Uuid - transaction_date DateTime? @db.Timestamptz(6) - status String? - number String? - id_acc_contact String? @db.Uuid - company String? @db.Uuid - exchange_rate String? - total_amount BigInt? - remaining_credit BigInt? - tracking_categories String[] - currency String? - remote_created_at DateTime? @db.Timestamptz(6) - remote_updated_at DateTime? @db.Timestamptz(6) - payments String[] - applied_payments String[] - id_acc_accounting_period String? @db.Uuid - remote_id String? - modified_at DateTime @db.Timetz(6) - created_at DateTime @db.Timetz(6) - id_connection String @db.Uuid +model fs_permissions { + id_fs_permission String @id(map: "pk_fs_permissions") @db.Uuid + remote_id String? + user String? @db.Uuid + group String? @db.Uuid + type String? + roles String[] + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid } -model acc_expense_lines { - id_acc_expense_line String @id(map: "pk_acc_expense_lines") @db.Uuid - id_acc_expense String @db.Uuid - remote_id String? - net_amount BigInt? - currency String? - description String? - exchange_rate String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model fs_shared_links { + id_fs_shared_link String @id(map: "pk_fs_shared_links") @db.Uuid + url String? + download_url String? + scope String? + password_protected Boolean + password String? + expires_at DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid + id_fs_folder String? @db.Uuid + id_fs_file String? @db.Uuid + remote_id String? +} - @@index([id_acc_expense], map: "fk_acc_expense_expense_lines_index") +model fs_users { + id_fs_user String @id(map: "pk_fs_users") @db.Uuid + name String? + email String? + is_me Boolean + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid } -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_expenses { - id_acc_expense String @id(map: "pk_acc_expenses") @db.Uuid - transaction_date DateTime? @db.Timestamptz(6) - total_amount BigInt? - sub_total BigInt? - total_tax_amount BigInt? - currency String? - exchange_rate String? - memo String? - id_acc_account String? @db.Uuid - id_acc_contact String? @db.Uuid - id_acc_company_info String? @db.Uuid - remote_id String? - remote_created_at DateTime? @db.Timestamptz(6) - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid - tracking_categories String[] +model hris_bank_infos { + id_hris_bank_info String @id(map: "pk_hris_bank_infos") @db.Uuid + account_type String? + bank_name String? + account_number String? + routing_number String? + remote_id String? + remote_created_at DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + remote_was_deleted Boolean + id_connection String @db.Uuid + id_hris_employee String? @db.Uuid + hris_employees hris_employees? @relation(fields: [id_hris_employee], references: [id_hris_employee], onDelete: NoAction, onUpdate: NoAction, map: "fk_bank_infos_employeeid") - @@index([id_acc_account], map: "fk_acc_account_acc_expense_index") - @@index([id_acc_company_info], map: "fk_acc_expense_acc_company_index") - @@index([id_acc_contact], map: "fk_acc_expense_acc_contact_index") + @@index([id_hris_employee], map: "fkx_bank_infos_employeeid") } -model acc_income_statements { - id_acc_income_statement String @id(map: "pk_acc_income_statements") @db.Uuid - name String? - currency String? - start_period DateTime? @db.Timestamptz(6) - end_period DateTime? @db.Timestamptz(6) - gross_profit BigInt? - net_operating_income BigInt? - net_income BigInt? - remote_id String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid +model hris_benefits { + id_hris_benefit String @id(map: "pk_hris_benefits") @db.Uuid + remote_id String? + remote_created_at DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + remote_was_deleted Boolean + id_connection String @db.Uuid + provider_name String? + id_hris_employee String? @db.Uuid + employee_contribution BigInt? + company_contribution BigInt? + start_date DateTime? @db.Timestamptz(6) + end_date DateTime? @db.Timestamptz(6) + id_hris_employer_benefit String? @db.Uuid + hris_employer_benefits hris_employer_benefits? @relation(fields: [id_hris_employer_benefit], references: [id_hris_employer_benefit], onDelete: NoAction, onUpdate: NoAction, map: "fk_hris_benefit_employer_benefit_id") + hris_employees hris_employees? @relation(fields: [id_hris_employee], references: [id_hris_employee], onDelete: NoAction, onUpdate: NoAction, map: "fk_hris_benefits_employeeid") + + @@index([id_hris_employer_benefit], map: "fkx_hris_benefit_employer_benefit_id") + @@index([id_hris_employee], map: "fkx_hris_benefits_employeeid") +} + +model hris_companies { + id_hris_company String @id(map: "pk_hris_companies") @db.Uuid + legal_name String? + display_name String? + eins String[] + remote_id String? + remote_created_at DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + remote_was_deleted Boolean + id_connection String @db.Uuid + hris_employees hris_employees[] } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_invoices { - id_acc_invoice String @id(map: "pk_acc_invoices") @db.Uuid - type String? - number String? - issue_date DateTime? @db.Timestamptz(6) - due_date DateTime? @db.Timestamptz(6) - paid_on_date DateTime? @db.Timestamptz(6) - memo String? - currency String? - exchange_rate String? - total_discount BigInt? - sub_total BigInt? - status String? - total_tax_amount BigInt? - total_amount BigInt? - balance BigInt? - remote_updated_at DateTime? @db.Timestamptz(6) - remote_id String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid - id_acc_contact String? @db.Uuid - id_acc_accounting_period String? @db.Uuid - tracking_categories String[] +model hris_dependents { + id_hris_dependents String @id(map: "pk_hris_dependents") @db.Uuid + first_name String? + last_name String? + middle_name String? + relationship String? + date_of_birth DateTime? @db.Date + gender String? + phone_number String? + home_location String? @db.Uuid + is_student Boolean? + ssn String? + id_hris_employee String? @db.Uuid + remote_id String? + remote_created_at DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + remote_was_deleted Boolean + id_connection String @db.Uuid + hris_employees hris_employees? @relation(fields: [id_hris_employee], references: [id_hris_employee], onDelete: NoAction, onUpdate: NoAction, map: "fk_hris_dependant_hris_employee_id") + + @@index([id_hris_employee], map: "fkx_hris_dependant_hris_employee_id") +} + +model hris_employee_payroll_runs { + id_hris_employee_payroll_run String @id(map: "pk_hris_employee_payroll_runs") @db.Uuid + id_hris_employee String? @db.Uuid + id_hris_payroll_run String? @db.Uuid + gross_pay BigInt? + net_pay BigInt? + start_date DateTime? @db.Timestamptz(6) + end_date DateTime? @db.Timestamptz(6) + check_date DateTime? @db.Timestamptz(6) + remote_id String? + remote_created_at DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + remote_was_deleted Boolean + id_connection String @db.Uuid + hris_payroll_runs hris_payroll_runs? @relation(fields: [id_hris_payroll_run], references: [id_hris_payroll_run], onDelete: NoAction, onUpdate: NoAction, map: "fk_employee_payroll_run_payroll_run_id") + hris_employees hris_employees? @relation(fields: [id_hris_employee], references: [id_hris_employee], onDelete: NoAction, onUpdate: NoAction, map: "fk_hris_employee_payroll_run_employee_id") + hris_employee_payroll_runs_deductions hris_employee_payroll_runs_deductions[] + hris_employee_payroll_runs_earnings hris_employee_payroll_runs_earnings[] + hris_employee_payroll_runs_taxes hris_employee_payroll_runs_taxes[] + + @@index([id_hris_payroll_run], map: "fkx_employee_payroll_run_payroll_run_id") + @@index([id_hris_employee], map: "fkx_hris_employee_payroll_run_employee_id") +} + +model hris_employee_payroll_runs_deductions { + id_hris_employee_payroll_runs_deduction String @id(map: "pk_hris_employee_payroll_runs_deductions") @db.Uuid + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_hris_employee_payroll_run String? @db.Uuid + name String? + employee_deduction BigInt? + company_deduction BigInt? + hris_employee_payroll_runs hris_employee_payroll_runs? @relation(fields: [id_hris_employee_payroll_run], references: [id_hris_employee_payroll_run], onDelete: NoAction, onUpdate: NoAction, map: "fk_hris_employee_payroll_runs_deduction_hris_employee_payroll_i") + + @@index([id_hris_employee_payroll_run], map: "fkx_hris_employee_payroll_runs_deduction_hris_employee_payroll_") +} + +model hris_employee_payroll_runs_earnings { + id_hris_employee_payroll_runs_earning String @id(map: "pk_hris_employee_payroll_runs_earnings") @db.Uuid + amount BigInt? + type String? + id_hris_employee_payroll_run String? @db.Uuid + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + hris_employee_payroll_runs hris_employee_payroll_runs? @relation(fields: [id_hris_employee_payroll_run], references: [id_hris_employee_payroll_run], onDelete: NoAction, onUpdate: NoAction, map: "fk_hris_employee_payroll_runs_earning_hris_employee_payroll_run") + + @@index([id_hris_employee_payroll_run], map: "fkx_hris_employee_payroll_runs_earning_hris_employee_payroll_ru") +} + +model hris_employee_payroll_runs_taxes { + id_hris_employee_payroll_runs_tax String @id(map: "pk_hris_employee_payroll_runs_taxes") @db.Uuid + name String? + amount BigInt? + employer_tax Boolean? + id_hris_employee_payroll_run String? @db.Uuid + remote_id String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + hris_employee_payroll_runs hris_employee_payroll_runs? @relation(fields: [id_hris_employee_payroll_run], references: [id_hris_employee_payroll_run], onDelete: NoAction, onUpdate: NoAction, map: "fk_hris_employee_payroll_run_tax_hris_employee_payroll_run_id") - @@index([id_acc_accounting_period], map: "fk_acc_invoice_accounting_period_index") - @@index([id_acc_contact], map: "fk_invoice_contactid") + @@index([id_hris_employee_payroll_run], map: "fkx_hris_employee_payroll_run_tax_hris_employee_payroll_run_id") } -model acc_invoices_line_items { - id_acc_invoices_line_item String @id(map: "pk_acc_invoices_line_items") @db.Uuid - remote_id String? - description String? - unit_price BigInt? - quantity BigInt? - total_amount BigInt? - currency String? - exchange_rate String? - id_acc_invoice String @db.Uuid - id_acc_item String @db.Uuid - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid - acc_tracking_categories String[] +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model hris_employees { + id_hris_employee String @id(map: "pk_hris_employees") @db.Uuid + remote_id String? + remote_created_at DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + remote_was_deleted Boolean + id_connection String @db.Uuid + manager String? @db.Uuid + groups String[] + employee_number String? + id_hris_company String? @db.Uuid + first_name String? + last_name String? + preferred_name String? + display_full_name String? + username String? + work_email String? + personal_email String? + mobile_phone_number String? + employments String[] + ssn String? + gender String? + ethnicity String? + marital_status String? + date_of_birth DateTime? @db.Date + start_date DateTime? @db.Date + employment_status String? + termination_date DateTime? @db.Date + avatar_url String? + hris_bank_infos hris_bank_infos[] + hris_benefits hris_benefits[] + hris_dependents hris_dependents[] + hris_employee_payroll_runs hris_employee_payroll_runs[] + hris_companies hris_companies? @relation(fields: [id_hris_company], references: [id_hris_company], onDelete: NoAction, onUpdate: NoAction, map: "fk_employee_companyid") + hris_employments hris_employments[] + hris_time_off_balances hris_time_off_balances[] + hris_timesheet_entries hris_timesheet_entries[] + + @@index([id_hris_company], map: "fkx_employee_companyid") +} + +model hris_employer_benefits { + id_hris_employer_benefit String @id(map: "pk_hris_employer_benefits") @db.Uuid + id_connection String @db.Uuid + benefit_plan_type String? + name String? + description String? + deduction_code String? + remote_id String? + remote_created_at DateTime? @db.Timestamptz(6) + remote_was_deleted Boolean + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + hris_benefits hris_benefits[] +} + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model hris_employments { + id_hris_employment String @id(map: "pk_hris_employments") @db.Uuid + job_title String + pay_rate BigInt? + pay_period String? + pay_frequency String? + pay_currency String? + flsa_status String? + effective_date DateTime? @db.Date + employment_type String? + remote_id String? + remote_created_at DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + remote_was_deleted Boolean + id_connection String @db.Uuid + id_hris_pay_group String? @db.Uuid + id_hris_employee String? @db.Uuid + hris_employees hris_employees? @relation(fields: [id_hris_employee], references: [id_hris_employee], onDelete: NoAction, onUpdate: NoAction, map: "fk_107") + hris_pay_groups hris_pay_groups? @relation(fields: [id_hris_pay_group], references: [id_hris_pay_group], onDelete: NoAction, onUpdate: NoAction, map: "fk_employments_pay_group_id") - @@index([id_acc_invoice], map: "fk_acc_invoice_line_items_index") - @@index([id_acc_item], map: "fk_acc_items_lines_invoice_index") + @@index([id_hris_employee], map: "fk_2") + @@index([id_hris_pay_group], map: "fkx_employments_pay_group_id") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_items { - id_acc_item String @id(map: "pk_acc_items") @db.Uuid - name String? - status String? - unit_price BigInt? - purchase_price BigInt? - remote_updated_at DateTime? @db.Timestamptz(6) - remote_id String? - sales_account String? @db.Uuid - purchase_account String? @db.Uuid - id_acc_company_info String? @db.Uuid - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid - - @@index([purchase_account], map: "fk_acc_item_acc_account") - @@index([id_acc_company_info], map: "fk_acc_item_acc_company_infos") - @@index([sales_account], map: "fk_acc_items_sales_account") +model hris_groups { + id_hris_group String @id(map: "pk_hris_groups") @db.Uuid + parent_group String? @db.Uuid + name String? + type String? + remote_id String + remote_created_at DateTime @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + remote_was_deleted Boolean + id_connection String @db.Uuid } -model acc_journal_entries { - id_acc_journal_entry String @id(map: "pk_acc_journal_entries") @db.Uuid - transaction_date DateTime? @db.Timestamptz(6) - payments String[] - applied_payments String[] - memo String? - currency String? - exchange_rate String? - id_acc_company_info String @db.Uuid - journal_number String? - tracking_categories String[] - id_acc_accounting_period String? @db.Uuid - posting_status String? - remote_created_at DateTime? @db.Timestamptz(6) - remote_modiified_at DateTime? @db.Timestamptz(6) - id_connection String @db.Uuid - remote_id String - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - - @@index([id_acc_accounting_period], map: "fk_journal_entry_accounting_period") - @@index([id_acc_company_info], map: "fk_journal_entry_companyinfo") +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model hris_locations { + id_hris_location String @id(map: "pk_hris_locations") @db.Uuid + name String? + phone_number String? + street_1 String? + street_2 String? + city String? + state String? + id_hris_company String? @db.Uuid + id_hris_employee String? @db.Uuid + zip_code String? + country String? + location_type String? + remote_id String? + remote_created_at DateTime @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + remote_was_deleted Boolean + id_connection String @db.Uuid } -model acc_journal_entries_lines { - id_acc_journal_entries_line String @id(map: "pk_acc_journal_entries_lines") @db.Uuid - net_amount BigInt? - tracking_categories String[] - currency String? - description String? - company String? @db.Uuid - contact String? @db.Uuid - exchange_rate String? - remote_id String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_acc_journal_entry String @db.Uuid +model hris_pay_groups { + id_hris_pay_group String @id(map: "pk_hris_pay_groups") @db.Uuid + pay_group_name String? + remote_id String? + remote_created_at DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + remote_was_deleted Boolean + id_connection String @db.Uuid + hris_employments hris_employments[] +} - @@index([id_acc_journal_entry], map: "fk_journal_entries_entries_lines") +model hris_payroll_runs { + id_hris_payroll_run String @id(map: "pk_hris_payroll_runs") @db.Uuid + run_state String? + run_type String? + start_date DateTime? @db.Timestamptz(6) + end_date DateTime? @db.Timestamptz(6) + check_date DateTime? @db.Timestamptz(6) + remote_id String? + remote_created_at DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + remote_was_deleted Boolean + id_connection String @db.Uuid + hris_employee_payroll_runs hris_employee_payroll_runs[] } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_payments { - id_acc_payment String @id(map: "pk_acc_payments") @db.Uuid - id_acc_invoice String? @db.Uuid - transaction_date DateTime? @db.Timestamptz(6) - id_acc_contact String? @db.Uuid - id_acc_account String? @db.Uuid - currency String? - exchange_rate String? - total_amount BigInt? - type String? - remote_updated_at DateTime? @db.Timestamptz(6) - id_acc_company_info String? @db.Uuid - id_acc_accounting_period String? @db.Uuid - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid - tracking_categories String[] +model hris_time_off { + id_hris_time_off String @id(map: "pk_hris_time_off") @db.Uuid + employee String? @db.Uuid + approver String? @db.Uuid + status String? + employee_note String? + units String? + amount BigInt? + request_type String? + start_time DateTime? @db.Timestamptz(6) + end_time DateTime? @db.Timestamptz(6) + remote_id String? + remote_created_at DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + remote_was_deleted Boolean + id_connection String @db.Uuid +} - @@index([id_acc_account], map: "fk_acc_payment_acc_account_index") - @@index([id_acc_company_info], map: "fk_acc_payment_acc_company_index") - @@index([id_acc_contact], map: "fk_acc_payment_acc_contact") - @@index([id_acc_accounting_period], map: "fk_acc_payment_accounting_period_index") - @@index([id_acc_invoice], map: "fk_acc_payment_invoiceid") +model hris_time_off_balances { + id_hris_time_off_balance String @id(map: "pk_hris_time_off_balances") @db.Uuid + balance BigInt? + id_hris_employee String? @db.Uuid + used BigInt? + policy_type String? + remote_id String? + remote_created_at DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + remote_was_deleted Boolean + id_connection String @db.Uuid + hris_employees hris_employees? @relation(fields: [id_hris_employee], references: [id_hris_employee], onDelete: NoAction, onUpdate: NoAction, map: "fk_hris_timeoff_balance_hris_employee_id") + + @@index([id_hris_employee], map: "fkx_hris_timeoff_balance_hris_employee_id") +} + +model hris_timesheet_entries { + id_hris_timesheet_entry String @id(map: "pk_hris_timesheet_entries") @db.Uuid + hours_worked BigInt? + start_time DateTime? @db.Timestamptz(6) + end_time DateTime? @db.Timestamptz(6) + id_hris_employee String? @db.Uuid + remote_id String? + remote_created_at DateTime? @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + remote_was_deleted Boolean + id_connection String @db.Uuid + hris_employees hris_employees? @relation(fields: [id_hris_employee], references: [id_hris_employee], onDelete: NoAction, onUpdate: NoAction, map: "fk_timesheet_entry_employee_id") + + @@index([id_hris_employee], map: "fkx_timesheet_entry_employee_id") } -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_payments_line_items { - acc_payments_line_item String @id(map: "pk_acc_payments_line_items") @db.Uuid - id_acc_payment String @db.Uuid - applied_amount BigInt? - applied_date DateTime? @db.Timestamptz(6) - related_object_id String? @db.Uuid - related_object_type String? - remote_id String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid +model invite_links { + id_invite_link String @id(map: "pk_invite_links") @db.Uuid + status String + email String? + id_linked_user String @db.Uuid + displayed_verticals String[] + displayed_providers String[] + linked_users linked_users @relation(fields: [id_linked_user], references: [id_linked_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_37") - @@index([id_acc_payment], map: "fk_acc_payment_line_items_index") + @@index([id_linked_user], map: "fk_invite_link_linkeduserid") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_phone_numbers { - id_acc_phone_number String @id(map: "pk_acc_phone_numbers") @db.Uuid - number String? - type String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_acc_company_info String? @db.Uuid - id_acc_contact String @db.Uuid - id_connection String @db.Uuid +model jobs_status_history { + id_jobs_status_history String @id(map: "pk_jobs_status_history") @db.Uuid + timestamp DateTime @default(now()) @db.Timestamptz(6) + previous_status String + new_status String + id_event String @db.Uuid + events events @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_4") - @@index([id_acc_contact], map: "fk_acc_phone_number_contact") - @@index([id_acc_company_info], map: "fk_company_infos_phone_number") + @@index([id_event], map: "id_job_jobs_status_history") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_purchase_orders { - id_acc_purchase_order String @id(map: "pk_acc_purchase_orders") @db.Uuid - remote_id String? - status String? - issue_date DateTime? @db.Timestamptz(6) - purchase_order_number String? - delivery_date DateTime? @db.Timestamptz(6) - delivery_address String? @db.Uuid - customer String? @db.Uuid - vendor String? @db.Uuid - memo String? - company String? @db.Uuid - total_amount BigInt? - currency String? - exchange_rate String? - tracking_categories String[] - remote_created_at DateTime? @db.Timestamptz(6) - remote_updated_at DateTime? @db.Timestamptz(6) - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid - id_acc_accounting_period String? @db.Uuid +model linked_users { + id_linked_user String @id(map: "key_id_linked_users") @db.Uuid + linked_user_origin_id String + alias String + id_project String @db.Uuid + connections connections[] + events events[] + invite_links invite_links[] + projects projects @relation(fields: [id_project], references: [id_project], onDelete: NoAction, onUpdate: NoAction, map: "fk_10") - @@index([id_acc_accounting_period], map: "fk_purchaseorder_accountingperiod") + @@index([id_project], map: "fk_proectid_linked_users") } -model acc_purchase_orders_line_items { - id_acc_purchase_orders_line_item String @id(map: "pk_acc_purchase_orders_line_items") @db.Uuid - id_acc_purchase_order String @db.Uuid - remote_id String? - modified_at DateTime @db.Timestamptz(6) - created_at DateTime @db.Timestamptz(6) - description String? - unit_price BigInt? - quantity BigInt? - tracking_categories String[] - tax_amount BigInt? - total_line_amount BigInt? - currency String? - exchange_rate String? - id_acc_account String? @db.Uuid - id_acc_company String? @db.Uuid - - @@index([id_acc_purchase_order], map: "fk_purchaseorder_purchaseorderlineitems") +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model managed_webhooks { + id_managed_webhook String @id(map: "pk_managed_webhooks") @db.Uuid + active Boolean + id_connection String @db.Uuid + endpoint String @db.Uuid + api_version String? + active_events String[] + remote_signing_secret String? + modified_at DateTime @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) } -model acc_report_items { - id_acc_report_item String @id(map: "pk_acc_report_items") @db.Uuid - name String? - value BigInt? - company String? @db.Uuid - parent_item String? @db.Uuid - remote_id String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model projects { + id_project String @id(map: "pk_projects") @db.Uuid + name String + sync_mode String + pull_frequency BigInt? + redirect_url String? + id_user String @db.Uuid + id_connector_set String @db.Uuid + api_keys api_keys[] + connections connections[] + linked_users linked_users[] + users users @relation(fields: [id_user], references: [id_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_46_1") + connector_sets connector_sets @relation(fields: [id_connector_set], references: [id_connector_set], onDelete: NoAction, onUpdate: NoAction, map: "fk_project_connectorsetid") + + @@index([id_connector_set], map: "fk_connectors_sets") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_tax_rates { - id_acc_tax_rate String @id(map: "pk_acc_tax_rates") @db.Uuid - remote_id String? - description String? - total_tax_ratge BigInt? - effective_tax_rate BigInt? - company String? @db.Uuid - id_connection String @db.Uuid - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) +model remote_data { + id_remote_data String @id(map: "pk_remote_data") @db.Uuid + ressource_owner_id String? @unique(map: "force_unique_ressourceownerid") @db.Uuid + format String? + data String? + created_at DateTime? @db.Timestamptz(6) } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_tracking_categories { - id_acc_tracking_category String @id(map: "pk_acc_tracking_categories") @db.Uuid - remote_id String? - name String? - status String? - category_type String? - parent_category String? @db.Uuid - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid +model tcg_accounts { + id_tcg_account String @id(map: "pk_tcg_account") @db.Uuid + remote_id String? + name String? + domains String[] + remote_platform String? + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_linked_user String? @db.Uuid + id_connection String @db.Uuid + tcg_contacts tcg_contacts[] } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_transactions { - id_acc_transaction String @id(map: "pk_acc_transactions") @db.Uuid - transaction_type String? - number BigInt? - transaction_date DateTime? @db.Timestamptz(6) - total_amount String? - exchange_rate String? - currency String? - tracking_categories String[] - id_acc_account String? @db.Uuid - id_acc_contact String? @db.Uuid - id_acc_company_info String? @db.Uuid - id_acc_accounting_period String? @db.Uuid - remote_id String - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid +model tcg_attachments { + id_tcg_attachment String @id(map: "pk_tcg_attachments") @db.Uuid + remote_id String? + remote_platform String? + file_name String? + file_url String? + uploader String @db.Uuid + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_linked_user String? @db.Uuid + id_tcg_ticket String? @db.Uuid + id_tcg_comment String? @db.Uuid + id_connection String @db.Uuid + tcg_tickets tcg_tickets? @relation(fields: [id_tcg_ticket], references: [id_tcg_ticket], onDelete: NoAction, onUpdate: NoAction, map: "fk_50") + tcg_comments tcg_comments? @relation(fields: [id_tcg_comment], references: [id_tcg_comment], onDelete: NoAction, onUpdate: NoAction, map: "fk_51") + + @@index([id_tcg_comment], map: "fk_tcg_attachment_tcg_commentid") + @@index([id_tcg_ticket], map: "fk_tcg_attachment_tcg_ticketid") +} + +model tcg_collections { + id_tcg_collection String @id(map: "pk_tcg_collections") @db.Uuid + name String? + description String? + remote_id String? + remote_platform String? + collection_type String? + parent_collection String? @db.Uuid + id_tcg_ticket String? @db.Uuid + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_linked_user String @db.Uuid + id_connection String @db.Uuid } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_transactions_lines_items { - id_acc_transactions_lines_item String @id(map: "pk_acc_transactions_lines_items") @db.Uuid - memo String? - unit_price String? - quantity String? - total_line_amount String? - id_acc_tax_rate String? @db.Uuid - currency String? - exchange_rate String? - tracking_categories String[] - id_acc_company_info String? @db.Uuid - id_acc_item String? @db.Uuid - id_acc_account String? @db.Uuid - remote_id String? - created_at DateTime @db.Timetz(6) - modified_at DateTime @db.Timetz(6) - id_acc_transaction String? @db.Uuid +model tcg_comments { + id_tcg_comment String @id(map: "pk_tcg_comments") @db.Uuid + body String? + html_body String? + is_private Boolean? + remote_id String? + remote_platform String? + creator_type String? + id_tcg_attachment String[] + id_tcg_ticket String? @db.Uuid + id_tcg_contact String? @db.Uuid + id_tcg_user String? @db.Uuid + id_linked_user String? @db.Uuid + created_at DateTime? @db.Timestamptz(6) + modified_at DateTime? @db.Timestamptz(6) + id_connection String @db.Uuid + tcg_attachments tcg_attachments[] + tcg_tickets tcg_tickets? @relation(fields: [id_tcg_ticket], references: [id_tcg_ticket], onDelete: NoAction, onUpdate: NoAction, map: "fk_40_1") + tcg_contacts tcg_contacts? @relation(fields: [id_tcg_contact], references: [id_tcg_contact], onDelete: NoAction, onUpdate: NoAction, map: "fk_41") + tcg_users tcg_users? @relation(fields: [id_tcg_user], references: [id_tcg_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_42") + + @@index([id_tcg_contact], map: "fk_tcg_comment_tcg_contact") + @@index([id_tcg_ticket], map: "fk_tcg_comment_tcg_ticket") + @@index([id_tcg_user], map: "fk_tcg_comment_tcg_userid") +} + +model tcg_contacts { + id_tcg_contact String @id(map: "pk_tcg_contact") @db.Uuid + name String? + email_address String? + phone_number String? + details String? + remote_id String? + remote_platform String? + created_at DateTime? @db.Timestamptz(6) + modified_at DateTime? @db.Timestamptz(6) + id_tcg_account String? @db.Uuid + id_linked_user String? @db.Uuid + id_connection String @db.Uuid + tcg_comments tcg_comments[] + tcg_accounts tcg_accounts? @relation(fields: [id_tcg_account], references: [id_tcg_account], onDelete: NoAction, onUpdate: NoAction, map: "fk_49") - @@index([id_acc_transaction], map: "fk_acc_transactions_lineitems") + @@index([id_tcg_account], map: "fk_tcg_contact_tcg_account_id") } -model acc_vendor_credit_lines { - id_acc_vendor_credit_line String @id(map: "pk_acc_vendor_credit_lines") @db.Uuid - net_amount BigInt? - tracking_categories String[] - description String? - account String? @db.Uuid - id_acc_account String? @db.Uuid - exchange_rate String? - id_acc_company_info String? @db.Uuid - remote_id String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - id_acc_vendor_credit String? @db.Uuid -} +model tcg_tags { + id_tcg_tag String @id(map: "pk_tcg_tags") @db.Uuid + name String? + remote_id String? + remote_platform String? + id_tcg_ticket String? @db.Uuid + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_linked_user String? @db.Uuid + id_connection String @db.Uuid + tcg_tickets tcg_tickets? @relation(fields: [id_tcg_ticket], references: [id_tcg_ticket], onDelete: NoAction, onUpdate: NoAction, map: "fk_48") -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model acc_vendor_credits { - id_acc_vendor_credit String @id(map: "pk_acc_vendor_credits") @db.Uuid - id_connection String @db.Uuid - remote_id String? - created_at DateTime @db.Timestamptz(6) - modified_at DateTime @db.Timestamptz(6) - number String? - transaction_date DateTime? @db.Timestamptz(6) - vendor String? @db.Uuid - total_amount BigInt? - currency String? - exchange_rate String? - company String? @db.Uuid - tracking_categories String[] - accounting_period String? @db.Uuid + @@index([id_tcg_ticket], map: "fk_tcg_tag_tcg_ticketid") } -model ecom_customers { - id_ecom_customer String @id(map: "pk_ecom_customers") @db.Uuid - remote_id String? - email String? - first_name String? - last_name String? - phone_number String? - modified_at DateTime @db.Timestamptz(6) - created_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid - remote_deleted Boolean - ecom_addresses ecom_addresses[] - ecom_orders ecom_orders[] +model tcg_teams { + id_tcg_team String @id(map: "pk_tcg_teams") @db.Uuid + remote_id String? + remote_platform String? + name String? + description String? + created_at DateTime @db.Timestamp(6) + modified_at DateTime @db.Timestamp(6) + id_linked_user String? @db.Uuid + id_connection String @db.Uuid } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model ecom_fulfilments { - id_ecom_fulfilment String @id(map: "pk_ecom_fulfilments") @db.Uuid - carrier String? - tracking_urls String[] - tracking_numbers String[] - items Json? - remote_id String? - id_ecom_order String? @db.Uuid - id_connection String @db.Uuid - modified_at DateTime @db.Timestamptz(6) - created_at DateTime @db.Timestamptz(6) - remote_deleted Boolean - ecom_orders ecom_orders? @relation(fields: [id_ecom_order], references: [id_ecom_order], onDelete: NoAction, onUpdate: NoAction, map: "fk_ecom_order_fulfilment") +model tcg_tickets { + id_tcg_ticket String @id(map: "pk_tcg_tickets") @db.Uuid + name String? + status String? + description String? + due_date DateTime? @db.Timestamptz(6) + ticket_type String? + parent_ticket String? @db.Uuid + tags String[] + collections String[] + completed_at DateTime? @db.Timestamptz(6) + priority String? + assigned_to String[] + remote_id String? + remote_platform String? + creator_type String? + id_tcg_user String? @db.Uuid + id_linked_user String? @db.Uuid + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + id_connection String @db.Uuid + tcg_attachments tcg_attachments[] + tcg_comments tcg_comments[] + tcg_tags tcg_tags[] - @@index([id_ecom_order], map: "fk_index_ecom_order_fulfilment") + @@index([id_tcg_user], map: "fk_tcg_ticket_tcg_user") } -model ecom_orders { - id_ecom_order String @id(map: "pk_ecom_orders") @db.Uuid - order_status String? - order_number String? - payment_status String? - currency String? - total_price BigInt? - total_discount BigInt? - total_shipping BigInt? - total_tax BigInt? - fulfillment_status String? - remote_id String? - id_ecom_customer String? @db.Uuid - id_connection String @db.Uuid - modified_at DateTime @db.Timestamptz(6) - created_at DateTime @db.Timestamptz(6) - remote_deleted Boolean - ecom_addresses ecom_addresses[] - ecom_fulfilments ecom_fulfilments[] - ecom_customers ecom_customers? @relation(fields: [id_ecom_customer], references: [id_ecom_customer], onDelete: NoAction, onUpdate: NoAction, map: "fk_ecom_customer_orders") +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model tcg_users { + id_tcg_user String @id(map: "pk_tcg_users") @db.Uuid + name String? + email_address String? + remote_id String? + remote_platform String? + teams String[] + id_linked_user String? @db.Uuid + id_connection String @db.Uuid + created_at DateTime? @db.Timestamptz(6) + modified_at DateTime? @db.Timestamptz(6) + tcg_comments tcg_comments[] +} - @@index([id_ecom_customer], map: "fk_index_ecom_customer_orders") +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model users { + id_user String @id(map: "pk_users") @db.Uuid + identification_strategy String + email String? @unique(map: "unique_email") + password_hash String? + first_name String + last_name String + id_stytch String? @unique(map: "force_stytch_id_unique") + created_at DateTime @default(now()) @db.Timestamptz(6) + modified_at DateTime @default(now()) @db.Timestamptz(6) + reset_token String? + reset_token_expires_at DateTime? @db.Timestamptz(6) + api_keys api_keys[] + projects projects[] } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model ecom_product_variants { - id_ecom_product_variant String @id(map: "pk_ecom_product_variants") @db.Uuid - id_connection String @db.Uuid - remote_id String? - title String? - price BigInt? - sku String? - options Json? - weight BigInt? - inventory_quantity BigInt? - id_ecom_product String? @db.Uuid - modified_at DateTime @db.Timestamptz(6) - created_at DateTime @db.Timestamptz(6) - remote_deleted Boolean - ecom_products ecom_products? @relation(fields: [id_ecom_product], references: [id_ecom_product], onDelete: NoAction, onUpdate: NoAction, map: "fk_ecom_products_variants") +model value { + id_value String @id(map: "pk_value") @db.Uuid + data String + id_entity String @db.Uuid + id_attribute String @db.Uuid + created_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) + attribute attribute @relation(fields: [id_attribute], references: [id_attribute], onDelete: NoAction, onUpdate: NoAction, map: "fk_33") + entity entity @relation(fields: [id_entity], references: [id_entity], onDelete: NoAction, onUpdate: NoAction, map: "fk_34") - @@index([id_ecom_product], map: "fk_index_ecom_products_variants") + @@index([id_attribute], map: "fk_value_attributeid") + @@index([id_entity], map: "fk_value_entityid") } -model ecom_products { - id_ecom_product String @id(map: "pk_ecom_products") @db.Uuid - remote_id String? - product_url String? - product_type String? - product_status String? - images_urls String[] - description String? - vendor String? - tags String[] - modified_at DateTime @db.Timestamptz(6) - created_at DateTime @db.Timestamptz(6) - id_connection String @db.Uuid - remote_deleted Boolean - ecom_product_variants ecom_product_variants[] +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model webhook_delivery_attempts { + id_webhook_delivery_attempt String @id(map: "pk_webhook_event") @db.Uuid + timestamp DateTime @db.Timestamptz(6) + status String + next_retry DateTime? @db.Timestamptz(6) + attempt_count BigInt + id_webhooks_payload String? @db.Uuid + id_webhook_endpoint String? @db.Uuid + id_event String? @db.Uuid + id_webhooks_reponse String? @db.Uuid + webhooks_payloads webhooks_payloads? @relation(fields: [id_webhooks_payload], references: [id_webhooks_payload], onDelete: NoAction, onUpdate: NoAction, map: "fk_38_1") + webhook_endpoints webhook_endpoints? @relation(fields: [id_webhook_endpoint], references: [id_webhook_endpoint], onDelete: NoAction, onUpdate: NoAction, map: "fk_38_2") + events events? @relation(fields: [id_event], references: [id_event], onDelete: NoAction, onUpdate: NoAction, map: "fk_39") + webhooks_reponses webhooks_reponses? @relation(fields: [id_webhooks_reponse], references: [id_webhooks_reponse], onDelete: NoAction, onUpdate: NoAction, map: "fk_40") + + @@index([id_webhooks_payload], map: "fk_we_payload_webhookid") + @@index([id_webhook_endpoint], map: "fk_we_webhookendpointid") + @@index([id_event], map: "fk_webhook_delivery_attempt_eventid") + @@index([id_webhooks_reponse], map: "fk_webhook_delivery_attempt_webhook_responseid") } /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model ecom_addresses { - id_ecom_address String @id(map: "pk_ecom_customer_addresses") @db.Uuid - address_type String? - street_1 String? - street_2 String? - city String? - state String? - postal_code String? - country String? - id_ecom_customer String @db.Uuid - modified_at DateTime @db.Timestamptz(6) - created_at DateTime @db.Timestamptz(6) - remote_deleted Boolean - id_ecom_order String? @db.Uuid - ecom_customers ecom_customers @relation(fields: [id_ecom_customer], references: [id_ecom_customer], onDelete: NoAction, onUpdate: NoAction, map: "fk_ecom_customer_customeraddress") - ecom_orders ecom_orders? @relation(fields: [id_ecom_order], references: [id_ecom_order], onDelete: NoAction, onUpdate: NoAction, map: "fk_ecom_order_address") - - @@index([id_ecom_customer], map: "fk_index_ecom_customer_customeraddress") - @@index([id_ecom_order], map: "fk_index_fk_ecom_order_address") +model webhook_endpoints { + id_webhook_endpoint String @id(map: "pk_webhook_endpoint") @db.Uuid + endpoint_description String? + url String + secret String + active Boolean + created_at DateTime @db.Timestamptz(6) + scope String[] + id_project String @db.Uuid + last_update DateTime? @db.Timestamptz(6) + webhook_delivery_attempts webhook_delivery_attempts[] } -model ecom_fulfilment_orders { - id_ecom_fulfilment_order String @id(map: "pk_ecom_fulfilment_order") @db.Uuid +model webhooks_payloads { + id_webhooks_payload String @id(map: "pk_webhooks_payload") @db.Uuid + data Json @db.Json + webhook_delivery_attempts webhook_delivery_attempts[] } -model ecom_order_line_items { - id_ecom_order_line_item String @id(map: "pk_106") @db.Uuid +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model webhooks_reponses { + id_webhooks_reponse String @id(map: "pk_webhooks_reponse") @db.Uuid + http_response_data String + http_status_code String + webhook_delivery_attempts webhook_delivery_attempts[] } diff --git a/packages/api/scripts/init.sql b/packages/api/scripts/init.sql index 95223ae23..c7d15863b 100644 --- a/packages/api/scripts/init.sql +++ b/packages/api/scripts/init.sql @@ -1,6 +1,3 @@ --- ************************* SqlDBM: PostgreSQL ************************* --- *********** Generated by SqlDBM: Panora_DB by rf@panora.dev ********** - -- ************************************** webhooks_reponses CREATE TABLE webhooks_reponses @@ -10,8 +7,10 @@ CREATE TABLE webhooks_reponses http_status_code text NOT NULL, CONSTRAINT PK_webhooks_reponse PRIMARY KEY ( id_webhooks_reponse ) ); + COMMENT ON COLUMN webhooks_reponses.http_status_code IS 'anything that is not 2xx is failed, and leads to retry'; + -- ************************************** webhooks_payloads CREATE TABLE webhooks_payloads ( @@ -20,6 +19,7 @@ CREATE TABLE webhooks_payloads CONSTRAINT PK_webhooks_payload PRIMARY KEY ( id_webhooks_payload ) ); + -- ************************************** webhook_endpoints CREATE TABLE webhook_endpoints ( @@ -28,17 +28,19 @@ CREATE TABLE webhook_endpoints url text NOT NULL, secret text NOT NULL, active boolean NOT NULL, - created_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, "scope" text[] NULL, id_project uuid NOT NULL, - last_update timestamp NULL, + last_update timestamp with time zone NULL, CONSTRAINT PK_webhook_endpoint PRIMARY KEY ( id_webhook_endpoint ) ); + COMMENT ON COLUMN webhook_endpoints.endpoint_description IS 'An optional description of what the webhook is used for'; COMMENT ON COLUMN webhook_endpoints.secret IS 'a shared secret for secure communication'; COMMENT ON COLUMN webhook_endpoints.active IS 'a flag indicating whether the webhook is active or not'; COMMENT ON COLUMN webhook_endpoints."scope" IS 'stringified array with events,'; + -- ************************************** users CREATE TABLE users ( @@ -49,21 +51,25 @@ CREATE TABLE users first_name text NOT NULL, last_name text NOT NULL, id_stytch text NULL, - created_at timestamp NOT NULL DEFAULT NOW(), - modified_at timestamp NOT NULL DEFAULT NOW(), + created_at timestamp with time zone NOT NULL DEFAULT NOW(), + modified_at timestamp with time zone NOT NULL DEFAULT NOW(), reset_token text NULL, reset_token_expires_at timestamp with time zone NULL, CONSTRAINT PK_users PRIMARY KEY ( id_user ), CONSTRAINT force_stytch_id_unique UNIQUE ( id_stytch ), CONSTRAINT unique_email UNIQUE ( email ) ); + COMMENT ON COLUMN users.identification_strategy IS 'can be: + PANORA_SELF_HOSTED STYTCH_B2B STYTCH_B2C'; COMMENT ON COLUMN users.created_at IS 'DEFAULT NOW() to automatically insert a value if nothing supplied'; + COMMENT ON CONSTRAINT force_stytch_id_unique ON users IS 'force unique on stytch id'; + -- ************************************** tcg_users CREATE TABLE tcg_users ( @@ -75,13 +81,16 @@ CREATE TABLE tcg_users teams text[] NULL, id_linked_user uuid NULL, id_connection uuid NOT NULL, - created_at timestamp NULL, - modified_at timestamp NULL, + created_at timestamp with time zone NULL, + modified_at timestamp with time zone NULL, CONSTRAINT PK_tcg_users PRIMARY KEY ( id_tcg_user ) ); + COMMENT ON TABLE tcg_users IS 'The User object is used to represent an employee within a company.'; + COMMENT ON COLUMN tcg_users.teams IS 'array of id_tcg_team. Teams the support agent belongs to.'; + -- ************************************** tcg_teams CREATE TABLE tcg_teams ( @@ -97,6 +106,7 @@ CREATE TABLE tcg_teams CONSTRAINT PK_tcg_teams PRIMARY KEY ( id_tcg_team ) ); + -- ************************************** tcg_collections CREATE TABLE tcg_collections ( @@ -108,13 +118,14 @@ CREATE TABLE tcg_collections collection_type text NULL, parent_collection uuid NULL, id_tcg_ticket uuid NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, id_linked_user uuid NOT NULL, id_connection uuid NOT NULL, CONSTRAINT PK_tcg_collections PRIMARY KEY ( id_tcg_collection ) ); + -- ************************************** tcg_accounts CREATE TABLE tcg_accounts ( @@ -123,14 +134,16 @@ CREATE TABLE tcg_accounts name text NULL, domains text[] NULL, remote_platform text NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, id_linked_user uuid NULL, id_connection uuid NOT NULL, CONSTRAINT PK_tcg_account PRIMARY KEY ( id_tcg_account ) ); + COMMENT ON COLUMN tcg_accounts.name IS 'company or customer name'; + -- ************************************** remote_data CREATE TABLE remote_data ( @@ -138,13 +151,15 @@ CREATE TABLE remote_data ressource_owner_id uuid NULL, "format" text NULL, data text NULL, - created_at timestamp NULL, + created_at timestamp with time zone NULL, CONSTRAINT PK_remote_data PRIMARY KEY ( id_remote_data ), CONSTRAINT Force_Unique_ressourceOwnerId UNIQUE ( ressource_owner_id ) ); + COMMENT ON COLUMN remote_data.ressource_owner_id IS 'uuid of the unified object that owns this remote data. UUID of the contact, or deal , etc...'; COMMENT ON COLUMN remote_data."format" IS 'can be json, xml'; + -- ************************************** managed_webhooks CREATE TABLE managed_webhooks ( @@ -155,13 +170,145 @@ CREATE TABLE managed_webhooks api_version text NULL, active_events text[] NULL, remote_signing_secret text NULL, - modified_at timestamp NOT NULL, - created_at timestamp NOT NULL, + modified_at timestamp with time zone NOT NULL, + created_at timestamp with time zone NOT NULL, CONSTRAINT PK_managed_webhooks PRIMARY KEY ( id_managed_webhook ) ); + COMMENT ON COLUMN managed_webhooks.endpoint IS 'UUID that will be used in the final URL to help identify where to route data ex: api.panora.dev/mw/{managed_webhooks.endpoint}'; +-- ************************************** hris_time_off +CREATE TABLE hris_time_off +( + id_hris_time_off uuid NOT NULL, + employee uuid NULL, + approver uuid NULL, + status text NULL, + employee_note text NULL, + units text NULL, + amount bigint NULL, + request_type text NULL, + start_time timestamp with time zone NULL, + end_time timestamp with time zone NULL, + remote_id text NULL, + remote_created_at timestamp with time zone NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, + remote_was_deleted boolean NOT NULL, + id_connection uuid NOT NULL, + CONSTRAINT PK_hris_time_off PRIMARY KEY ( id_hris_time_off ) +); +COMMENT ON COLUMN hris_time_off.employee IS 'id_hris_employee of the employee requesting the time off'; +COMMENT ON COLUMN hris_time_off.approver IS 'id_hris_employee of the manager approving the time off'; + +-- ************************************** hris_payroll_runs +CREATE TABLE hris_payroll_runs +( + id_hris_payroll_run uuid NOT NULL, + run_state text NULL, + run_type text NULL, + start_date timestamp with time zone NULL, + end_date timestamp with time zone NULL, + check_date timestamp with time zone NULL, + remote_id text NULL, + remote_created_at timestamp with time zone NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, + remote_was_deleted boolean NOT NULL, + id_connection uuid NOT NULL, + CONSTRAINT PK_hris_payroll_runs PRIMARY KEY ( id_hris_payroll_run ) +); + +-- ************************************** hris_pay_groups +CREATE TABLE hris_pay_groups +( + id_hris_pay_group uuid NOT NULL, + pay_group_name text NULL, + remote_id text NULL, + remote_created_at timestamp with time zone NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, + remote_was_deleted boolean NOT NULL, + id_connection uuid NOT NULL, + CONSTRAINT PK_hris_pay_groups PRIMARY KEY ( id_hris_pay_group ) +); + +-- ************************************** hris_locations +CREATE TABLE hris_locations +( + id_hris_location uuid NOT NULL, + name text NULL, + phone_number text NULL, + street_1 text NULL, + street_2 text NULL, + city text NULL, + "state" text NULL, + id_hris_company uuid NULL, + id_hris_employee uuid NULL, + zip_code text NULL, + country text NULL, + location_type text NULL, + remote_id text NULL, + remote_created_at timestamp with time zone NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, + remote_was_deleted boolean NOT NULL, + id_connection uuid NOT NULL, + CONSTRAINT PK_hris_locations PRIMARY KEY ( id_hris_location ) +); +COMMENT ON COLUMN hris_locations.location_type IS 'HOME, WORK'; + +-- ************************************** hris_groups +CREATE TABLE hris_groups +( + id_hris_group uuid NOT NULL, + parent_group uuid NULL, + name text NULL, + type text NULL, + remote_id text NOT NULL, + remote_created_at timestamp with time zone NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, + remote_was_deleted boolean NOT NULL, + id_connection uuid NOT NULL, + CONSTRAINT PK_hris_groups PRIMARY KEY ( id_hris_group ) +); +COMMENT ON COLUMN hris_groups.parent_group IS 'id_hris_group of parent group'; + +-- ************************************** hris_employer_benefits +CREATE TABLE hris_employer_benefits +( + id_hris_employer_benefit uuid NOT NULL, + id_connection uuid NOT NULL, + benefit_plan_type text NULL, + name text NULL, + description text NULL, + deduction_code text NULL, + remote_id text NULL, + remote_created_at timestamp with time zone NULL, + remote_was_deleted boolean NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, + CONSTRAINT PK_hris_employer_benefits PRIMARY KEY ( id_hris_employer_benefit ) +); + +-- ************************************** hris_companies +CREATE TABLE hris_companies +( + id_hris_company uuid NOT NULL, + legal_name text NULL, + display_name text NULL, + eins text[] NULL, + remote_id text NULL, + remote_created_at timestamp with time zone NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, + remote_was_deleted boolean NOT NULL, + id_connection uuid NOT NULL, + CONSTRAINT PK_hris_companies PRIMARY KEY ( id_hris_company ) +); + -- ************************************** fs_users CREATE TABLE fs_users ( @@ -170,12 +317,13 @@ CREATE TABLE fs_users email text NULL, is_me boolean NOT NULL, remote_id text NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, id_connection uuid NOT NULL, CONSTRAINT PK_fs_users PRIMARY KEY ( id_fs_user ) ); + -- ************************************** fs_shared_links CREATE TABLE fs_shared_links ( @@ -194,9 +342,11 @@ CREATE TABLE fs_shared_links remote_id text NULL, CONSTRAINT PK_fs_shared_links PRIMARY KEY ( id_fs_shared_link ) ); + COMMENT ON COLUMN fs_shared_links."scope" IS 'can be public, or company depending on the link'; COMMENT ON COLUMN fs_shared_links.password IS 'encrypted password'; + -- ************************************** fs_permissions CREATE TABLE fs_permissions ( @@ -206,13 +356,15 @@ CREATE TABLE fs_permissions "group" uuid NULL, type text NULL, roles text[] NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, id_connection uuid NOT NULL, CONSTRAINT PK_fs_permissions PRIMARY KEY ( id_fs_permission ) ); + COMMENT ON COLUMN fs_permissions.roles IS 'read, write, owner'; + -- ************************************** fs_groups CREATE TABLE fs_groups ( @@ -221,27 +373,30 @@ CREATE TABLE fs_groups users text[] NULL, remote_id text NULL, remote_was_deleted boolean NOT NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, id_connection uuid NOT NULL, CONSTRAINT PK_fs_groups PRIMARY KEY ( id_fs_group ) ); + COMMENT ON COLUMN fs_groups.remote_was_deleted IS 'set to true'; + -- ************************************** fs_drives CREATE TABLE fs_drives ( id_fs_drive uuid NOT NULL, drive_url text NULL, name text NULL, - remote_created_at timestamp NULL, + remote_created_at timestamp with time zone NULL, remote_id text NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, id_connection uuid NOT NULL, CONSTRAINT PK_fs_drives PRIMARY KEY ( id_fs_drive ) ); + -- ************************************** entity CREATE TABLE entity ( @@ -251,8 +406,10 @@ CREATE TABLE entity modified_at timestamp with time zone NOT NULL, CONSTRAINT PK_entity PRIMARY KEY ( id_entity ) ); + COMMENT ON COLUMN entity.ressource_owner_id IS 'uuid of the ressource owner - can be a a crm_contact, a crm_deal, etc...'; + -- ************************************** ecom_products CREATE TABLE ecom_products ( @@ -272,6 +429,7 @@ CREATE TABLE ecom_products CONSTRAINT PK_ecom_products PRIMARY KEY ( id_ecom_product ) ); + -- ************************************** ecom_order_line_items CREATE TABLE ecom_order_line_items ( @@ -279,6 +437,7 @@ CREATE TABLE ecom_order_line_items CONSTRAINT PK_106 PRIMARY KEY ( id_ecom_order_line_item ) ); + -- ************************************** ecom_fulfilment_orders CREATE TABLE ecom_fulfilment_orders ( @@ -286,6 +445,7 @@ CREATE TABLE ecom_fulfilment_orders CONSTRAINT PK_ecom_fulfilment_order PRIMARY KEY ( id_ecom_fulfilment_order ) ); + -- ************************************** ecom_customers CREATE TABLE ecom_customers ( @@ -302,6 +462,7 @@ CREATE TABLE ecom_customers CONSTRAINT PK_ecom_customers PRIMARY KEY ( id_ecom_customer ) ); + -- ************************************** cs_values CREATE TABLE cs_values ( @@ -311,6 +472,7 @@ CREATE TABLE cs_values CONSTRAINT PK_ct_values PRIMARY KEY ( id_cs_value ) ); + -- ************************************** cs_entities CREATE TABLE cs_entities ( @@ -319,6 +481,7 @@ CREATE TABLE cs_entities CONSTRAINT PK_ct_entities PRIMARY KEY ( id_cs_entity ) ); + -- ************************************** cs_attributes CREATE TABLE cs_attributes ( @@ -329,14 +492,15 @@ CREATE TABLE cs_attributes CONSTRAINT PK_ct_attributes PRIMARY KEY ( id_cs_attribute ) ); + -- ************************************** crm_users CREATE TABLE crm_users ( id_crm_user uuid NOT NULL, name text NULL, email text NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, id_linked_user uuid NULL, remote_id text NULL, remote_platform text NULL, @@ -344,13 +508,14 @@ CREATE TABLE crm_users CONSTRAINT PK_crm_users PRIMARY KEY ( id_crm_user ) ); + -- ************************************** crm_deals_stages CREATE TABLE crm_deals_stages ( id_crm_deals_stage uuid NOT NULL, stage_name text NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, id_linked_user uuid NULL, remote_id text NULL, remote_platform text NULL, @@ -358,6 +523,7 @@ CREATE TABLE crm_deals_stages CONSTRAINT PK_crm_deal_stages PRIMARY KEY ( id_crm_deals_stage ) ); + -- ************************************** connector_sets CREATE TABLE connector_sets ( @@ -377,10 +543,14 @@ CREATE TABLE connector_sets tcg_github boolean NULL, ecom_woocommerce boolean NULL, ecom_shopify boolean NULL, - tcg_linear boolean NULL, -CONSTRAINT PK_project_connector PRIMARY KEY ( id_connector_set ) + ecom_amazon boolean NULL, + ecom_squarespace boolean NULL, + tcg_linear boolean NULL, + CONSTRAINT PK_project_connector PRIMARY KEY ( id_connector_set ) ); + + -- ************************************** connection_strategies CREATE TABLE connection_strategies ( @@ -390,10 +560,12 @@ CREATE TABLE connection_strategies id_project uuid NULL, CONSTRAINT PK_connection_strategies PRIMARY KEY ( id_connection_strategy ) ); + COMMENT ON COLUMN connection_strategies.id_connection_strategy IS 'Connection strategies are meant to overwrite default env variables for oauth strategies'; COMMENT ON COLUMN connection_strategies.status IS 'if the connection strategy should overwrite default strategy (from env)'; COMMENT ON COLUMN connection_strategies.type IS 'OAUTH2, API_KEY, PIPEDRIVE_CLOUD_OAUTH, PIPEDRIVE_CLOUD_API, HUBSPOT_CLOUD'; + -- ************************************** ats_users CREATE TABLE ats_users ( @@ -411,8 +583,10 @@ CREATE TABLE ats_users id_connection uuid NOT NULL, CONSTRAINT PK_ats_users PRIMARY KEY ( id_ats_user ) ); + COMMENT ON COLUMN ats_users.access_role IS 'The user''s role. Possible values include: SUPER_ADMIN, ADMIN, TEAM_MEMBER, LIMITED_TEAM_MEMBER, INTERVIEWER. In cases where there is no clear mapping, the original value passed through will be returned.'; + -- ************************************** ats_reject_reasons CREATE TABLE ats_reject_reasons ( @@ -425,6 +599,7 @@ CREATE TABLE ats_reject_reasons CONSTRAINT PK_ats_reject_reasons PRIMARY KEY ( id_ats_reject_reason ) ); + -- ************************************** ats_offices CREATE TABLE ats_offices ( @@ -437,8 +612,10 @@ CREATE TABLE ats_offices id_connection uuid NOT NULL, CONSTRAINT PK_ats_offices PRIMARY KEY ( id_ats_office ) ); + COMMENT ON TABLE ats_offices IS 'The Office object is used to represent an office within a company. A given Job has the Office ID in its offices field.'; + -- ************************************** ats_jobs CREATE TABLE ats_jobs ( @@ -461,7 +638,9 @@ CREATE TABLE ats_jobs id_connection uuid NOT NULL, CONSTRAINT PK_ats_jobs PRIMARY KEY ( id_ats_job ) ); + COMMENT ON TABLE ats_jobs IS 'The Job object can be used to track any jobs that are currently or will be open/closed for applications.'; + COMMENT ON COLUMN ats_jobs.description IS 'the jobs description'; COMMENT ON COLUMN ats_jobs.status IS 'The job''s status. Possible values include: OPEN, CLOSED, DRAFT, ARCHIVED, PENDING. In cases where there is no clear mapping, the original value passed through will be returned.'; COMMENT ON COLUMN ats_jobs.type IS 'The job''s type. Possible values include: POSTING, REQUISITION, PROFILE. In cases where there is no clear mapping, the original value passed through will be returned.'; @@ -470,6 +649,7 @@ COMMENT ON COLUMN ats_jobs.ats_offices IS 'IDs of Office objects for this Job.'; COMMENT ON COLUMN ats_jobs.managers IS 'IDs of RemoteUser objects that serve as hiring managers for this Job.'; COMMENT ON COLUMN ats_jobs.recruiters IS 'IDs of RemoteUser objects that serve as recruiters for this Job.'; + -- ************************************** ats_departments CREATE TABLE ats_departments ( @@ -482,6 +662,7 @@ CREATE TABLE ats_departments CONSTRAINT PK_ats_departments PRIMARY KEY ( id_ats_department ) ); + -- ************************************** ats_candidates CREATE TABLE ats_candidates ( @@ -503,6 +684,7 @@ CREATE TABLE ats_candidates id_connection uuid NOT NULL, CONSTRAINT PK_ats_candidates PRIMARY KEY ( id_ats_candidate ) ); + COMMENT ON COLUMN ats_candidates.first_name IS 'candidate''s first name.'; COMMENT ON COLUMN ats_candidates.last_name IS 'candidate''s last name.'; COMMENT ON COLUMN ats_candidates.company IS 'The candidate''s current company'; @@ -510,6 +692,7 @@ COMMENT ON COLUMN ats_candidates.title IS 'The candidate''s current title'; COMMENT ON COLUMN ats_candidates.email_reachable IS 'can the candidate be emailed'; COMMENT ON COLUMN ats_candidates.tags IS 'array of id_ats_candidate_tag'; + -- ************************************** ats_candidate_tags CREATE TABLE ats_candidate_tags ( @@ -522,6 +705,7 @@ CREATE TABLE ats_candidate_tags CONSTRAINT PK_ats_candidate_tags PRIMARY KEY ( id_ats_candidate_tag ) ); + -- ************************************** acc_vendor_credits CREATE TABLE acc_vendor_credits ( @@ -541,8 +725,10 @@ CREATE TABLE acc_vendor_credits accounting_period uuid NULL, CONSTRAINT PK_acc_vendor_credits PRIMARY KEY ( id_acc_vendor_credit ) ); + COMMENT ON COLUMN acc_vendor_credits.company IS 'The company the vendor credit belongs to.'; + -- ************************************** acc_vendor_credit_lines CREATE TABLE acc_vendor_credit_lines ( @@ -561,6 +747,7 @@ CREATE TABLE acc_vendor_credit_lines CONSTRAINT PK_acc_vendor_credit_lines PRIMARY KEY ( id_acc_vendor_credit_line ) ); + -- ************************************** acc_transactions CREATE TABLE acc_transactions ( @@ -582,8 +769,10 @@ CREATE TABLE acc_transactions id_connection uuid NOT NULL, CONSTRAINT PK_acc_transactions PRIMARY KEY ( id_acc_transaction ) ); + COMMENT ON TABLE acc_transactions IS 'Transactions The Transaction common model includes records of all types of transactions that do not appear in other common models. The type of transaction can be identified through the type field. More specifically, it will contain all types of transactions outside of: + Credit Notes Expenses Invoices @@ -591,8 +780,10 @@ Journal Entries Payments Purchase Orders Vendor Credits'; + COMMENT ON COLUMN acc_transactions.total_amount IS 'The total amount being paid after taxes.'; + -- ************************************** acc_tracking_categories CREATE TABLE acc_tracking_categories ( @@ -607,10 +798,12 @@ CREATE TABLE acc_tracking_categories id_connection uuid NOT NULL, CONSTRAINT PK_acc_tracking_categories PRIMARY KEY ( id_acc_tracking_category ) ); + COMMENT ON COLUMN acc_tracking_categories.status IS 'The tracking category''s status. Possible values include: ACTIVE, ARCHIVED. In cases where there is no clear mapping, the original value passed through will be returned.'; COMMENT ON COLUMN acc_tracking_categories.category_type IS 'The tracking category’s type. Possible values include: CLASS, DEPARTMENT. In cases where there is no clear mapping, the original value passed through will be returned.'; COMMENT ON COLUMN acc_tracking_categories.parent_category IS 'ID of the parent tracking category.'; + -- ************************************** acc_tax_rates CREATE TABLE acc_tax_rates ( @@ -625,9 +818,11 @@ CREATE TABLE acc_tax_rates modified_at timestamp with time zone NOT NULL, CONSTRAINT PK_acc_tax_rates PRIMARY KEY ( id_acc_tax_rate ) ); + COMMENT ON COLUMN acc_tax_rates.total_tax_ratge IS 'The tax’s total tax rate - sum of the tax components (not compounded).'; COMMENT ON COLUMN acc_tax_rates.company IS 'The subsidiary that the tax rate belongs to (in the case of multi-entity systems).'; + -- ************************************** acc_report_items CREATE TABLE acc_report_items ( @@ -642,6 +837,7 @@ CREATE TABLE acc_report_items CONSTRAINT PK_acc_report_items PRIMARY KEY ( id_acc_report_item ) ); + -- ************************************** acc_income_statements CREATE TABLE acc_income_statements ( @@ -660,6 +856,7 @@ CREATE TABLE acc_income_statements CONSTRAINT PK_acc_income_statements PRIMARY KEY ( id_acc_income_statement ) ); + -- ************************************** acc_credit_notes CREATE TABLE acc_credit_notes ( @@ -685,10 +882,12 @@ CREATE TABLE acc_credit_notes id_connection uuid NOT NULL, CONSTRAINT PK_acc_credit_notes PRIMARY KEY ( id_acc_credit_note ) ); + COMMENT ON COLUMN acc_credit_notes.status IS 'The credit note''s status. Possible values include: SUBMITTED, AUTHORIZED, PAID. In cases where there is no clear mapping, the original value passed through will be returned.'; COMMENT ON COLUMN acc_credit_notes."number" IS 'The credit note''s number.'; COMMENT ON COLUMN acc_credit_notes.payments IS 'array of id_acc_payment'; + -- ************************************** acc_company_infos CREATE TABLE acc_company_infos ( @@ -709,6 +908,7 @@ CREATE TABLE acc_company_infos CONSTRAINT PK_acc_company_infos PRIMARY KEY ( id_acc_company_info ) ); + -- ************************************** acc_cash_flow_statements CREATE TABLE acc_cash_flow_statements ( @@ -728,6 +928,7 @@ CREATE TABLE acc_cash_flow_statements CONSTRAINT PK_acc_cash_flow_statements PRIMARY KEY ( id_acc_cash_flow_statement ) ); + -- ************************************** acc_balance_sheets_report_items CREATE TABLE acc_balance_sheets_report_items ( @@ -741,8 +942,10 @@ CREATE TABLE acc_balance_sheets_report_items id_acc_company_info uuid NULL, CONSTRAINT PK_acc_balance_sheets_report_items PRIMARY KEY ( id_acc_balance_sheets_report_item ) ); + COMMENT ON COLUMN acc_balance_sheets_report_items.parent_item IS 'uuid of another id_acc_balance_sheets_report_item'; + -- ************************************** acc_accounting_periods CREATE TABLE acc_accounting_periods ( @@ -757,8 +960,10 @@ CREATE TABLE acc_accounting_periods id_connection uuid NOT NULL, CONSTRAINT PK_acc_accounting_periods PRIMARY KEY ( id_acc_accounting_period ) ); + COMMENT ON COLUMN acc_accounting_periods.status IS 'Possible values include: ACTIVE, INACTIVE. In cases where there is no clear mapping, the original value passed through will be returned.'; + -- ************************************** tcg_tickets CREATE TABLE tcg_tickets ( @@ -766,12 +971,12 @@ CREATE TABLE tcg_tickets name text NULL, status text NULL, description text NULL, - due_date timestamp NULL, + due_date timestamp with time zone NULL, ticket_type text NULL, parent_ticket uuid NULL, tags text[] NULL, collections text[] NULL, - completed_at timestamp NULL, + completed_at timestamp with time zone NULL, priority text NULL, assigned_to text[] NULL, remote_id text NULL, @@ -779,22 +984,26 @@ CREATE TABLE tcg_tickets creator_type text NULL, id_tcg_user uuid NULL, id_linked_user uuid NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, id_connection uuid NOT NULL, CONSTRAINT PK_tcg_tickets PRIMARY KEY ( id_tcg_ticket ) ); + CREATE INDEX FK_tcg_ticket_tcg_user ON tcg_tickets ( id_tcg_user ); + COMMENT ON COLUMN tcg_tickets.name IS 'Name of the ticket. Usually very short.'; COMMENT ON COLUMN tcg_tickets.status IS 'OPEN, CLOSED, IN_PROGRESS, ON_HOLD'; COMMENT ON COLUMN tcg_tickets.tags IS 'array of tags uuid'; COMMENT ON COLUMN tcg_tickets.assigned_to IS 'Employees assigned to this ticket. + It is a stringified array containing tcg_users'; COMMENT ON COLUMN tcg_tickets.id_tcg_user IS 'id of the user who created the ticket'; + -- ************************************** tcg_contacts CREATE TABLE tcg_contacts ( @@ -805,19 +1014,21 @@ CREATE TABLE tcg_contacts details text NULL, remote_id text NULL, remote_platform text NULL, - created_at timestamp NULL, - modified_at timestamp NULL, + created_at timestamp with time zone NULL, + modified_at timestamp with time zone NULL, id_tcg_account uuid NULL, id_linked_user uuid NULL, id_connection uuid NOT NULL, CONSTRAINT PK_tcg_contact PRIMARY KEY ( id_tcg_contact ), CONSTRAINT FK_49 FOREIGN KEY ( id_tcg_account ) REFERENCES tcg_accounts ( id_tcg_account ) ); + CREATE INDEX FK_tcg_contact_tcg_account_id ON tcg_contacts ( id_tcg_account ); + -- ************************************** projects CREATE TABLE projects ( @@ -832,14 +1043,60 @@ CREATE TABLE projects CONSTRAINT FK_project_connectorsetid FOREIGN KEY ( id_connector_set ) REFERENCES connector_sets ( id_connector_set ), CONSTRAINT FK_46_1 FOREIGN KEY ( id_user ) REFERENCES users ( id_user ) ); + CREATE INDEX FK_connectors_sets ON projects ( id_connector_set ); + COMMENT ON COLUMN projects.sync_mode IS 'Can be realtime or periodic_pull'; COMMENT ON COLUMN projects.pull_frequency IS 'Frequency in seconds for pulls + ex 3600 for one hour'; +-- ************************************** hris_employees +CREATE TABLE hris_employees +( + id_hris_employee uuid NOT NULL, + remote_id text NULL, + remote_created_at timestamp with time zone NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, + remote_was_deleted boolean NOT NULL, + id_connection uuid NOT NULL, + manager uuid NULL, + groups text[] NULL, + employee_number text NULL, + id_hris_company uuid NULL, + first_name text NULL, + last_name text NULL, + preferred_name text NULL, + display_full_name text NULL, + username text NULL, + work_email text NULL, + personal_email text NULL, + mobile_phone_number text NULL, + employments text[] NULL, + ssn text NULL, + gender text NULL, + ethnicity text NULL, + marital_status text NULL, + date_of_birth date NULL, + start_date date NULL, + employment_status text NULL, + termination_date date NULL, + avatar_url text NULL, + CONSTRAINT PK_hris_employees PRIMARY KEY ( id_hris_employee ), + CONSTRAINT FK_employee_companyId FOREIGN KEY ( id_hris_company ) REFERENCES hris_companies ( id_hris_company ) +); +CREATE INDEX FKX_employee_companyId ON hris_employees +( + id_hris_company +); +COMMENT ON COLUMN hris_employees.groups IS 'array of id_hris_group'; +COMMENT ON COLUMN hris_employees.employments IS 'array of id_hris_employment'; +COMMENT ON COLUMN hris_employees.gender IS 'The employee''s gender. Possible options are: MALE, FEMALE, NON-BINARY, OTHER, or PREFER_NOT_TO_DISCLOSE. If the original value doesn''t correspond to any of these categories, it will be returned as is.'; + -- ************************************** fs_folders CREATE TABLE fs_folders ( @@ -850,22 +1107,25 @@ CREATE TABLE fs_folders description text NULL, parent_folder uuid NULL, remote_id text NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, id_fs_drive uuid NULL, id_connection uuid NOT NULL, id_fs_permission uuid NULL, CONSTRAINT PK_fs_folders PRIMARY KEY ( id_fs_folder ) ); + CREATE INDEX FK_fs_folder_driveID ON fs_folders ( id_fs_drive ); + CREATE INDEX FK_fs_folder_permissionID ON fs_folders ( id_fs_permission ); + -- ************************************** ecom_product_variants CREATE TABLE ecom_product_variants ( @@ -885,12 +1145,15 @@ CREATE TABLE ecom_product_variants CONSTRAINT PK_ecom_product_variants PRIMARY KEY ( id_ecom_product_variant ), CONSTRAINT FK_ecom_products_variants FOREIGN KEY ( id_ecom_product ) REFERENCES ecom_products ( id_ecom_product ) ); + CREATE INDEX FK_index_ecom_products_variants ON ecom_product_variants ( id_ecom_product ); + COMMENT ON COLUMN ecom_product_variants.options IS 'an array of product options. ex [{color: blue}, {size: medium}] ...'; + -- ************************************** ecom_orders CREATE TABLE ecom_orders ( @@ -913,19 +1176,21 @@ CREATE TABLE ecom_orders CONSTRAINT PK_ecom_orders PRIMARY KEY ( id_ecom_order ), CONSTRAINT FK_ecom_customer_orders FOREIGN KEY ( id_ecom_customer ) REFERENCES ecom_customers ( id_ecom_customer ) ); + CREATE INDEX FK_index_ecom_customer_orders ON ecom_orders ( id_ecom_customer ); + -- ************************************** crm_contacts CREATE TABLE crm_contacts ( id_crm_contact uuid NOT NULL, first_name text NULL, last_name text NULL, - created_at timestamp NULL, - modified_at timestamp NULL, + created_at timestamp with time zone NULL, + modified_at timestamp with time zone NULL, remote_id text NULL, remote_platform text NULL, id_crm_user uuid NULL, @@ -934,12 +1199,15 @@ CREATE TABLE crm_contacts CONSTRAINT PK_crm_contacts PRIMARY KEY ( id_crm_contact ), CONSTRAINT FK_23 FOREIGN KEY ( id_crm_user ) REFERENCES crm_users ( id_crm_user ) ); + CREATE INDEX FK_crm_contact_userID ON crm_contacts ( id_crm_user ); + COMMENT ON COLUMN crm_contacts.remote_platform IS 'can be hubspot, zendesk, zoho...'; + -- ************************************** crm_companies CREATE TABLE crm_companies ( @@ -947,8 +1215,8 @@ CREATE TABLE crm_companies name text NULL, industry text NULL, number_of_employees bigint NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, remote_id text NULL, remote_platform text NULL, id_crm_user uuid NULL, @@ -957,11 +1225,13 @@ CREATE TABLE crm_companies CONSTRAINT PK_crm_companies PRIMARY KEY ( id_crm_company ), CONSTRAINT FK_24 FOREIGN KEY ( id_crm_user ) REFERENCES crm_users ( id_crm_user ) ); + CREATE INDEX FK_crm_company_crm_userID ON crm_companies ( id_crm_user ); + -- ************************************** attribute CREATE TABLE attribute ( @@ -982,23 +1252,29 @@ CREATE TABLE attribute CONSTRAINT PK_attribute PRIMARY KEY ( id_attribute ), CONSTRAINT FK_32 FOREIGN KEY ( id_entity ) REFERENCES entity ( id_entity ) ); + CREATE INDEX FK_attribute_entityID ON attribute ( id_entity ); + COMMENT ON COLUMN attribute.status IS 'NEED_REMOTE_ID LINKED'; COMMENT ON COLUMN attribute.ressource_owner_type IS 'ressource_owner type: + crm_deal, crm_contact'; COMMENT ON COLUMN attribute.slug IS 'Custom field name,ex : SIZE, AGE, MIDDLE_NAME, HAS_A_CAR ...'; COMMENT ON COLUMN attribute.description IS 'description of this custom field'; COMMENT ON COLUMN attribute.data_type IS 'INTEGER, STRING, BOOLEAN...'; COMMENT ON COLUMN attribute."source" IS 'can be hubspot, zendesk, etc'; COMMENT ON COLUMN attribute."scope" IS 'defines at which scope the ressource will be available + can be "ORGANIZATION", or "LINKED_USER"'; COMMENT ON COLUMN attribute.id_consumer IS 'Can be an organization iD , or linked user ID + id_linked_user'; + -- ************************************** ats_job_interview_stages CREATE TABLE ats_job_interview_stages ( @@ -1012,13 +1288,17 @@ CREATE TABLE ats_job_interview_stages id_connection uuid NOT NULL, CONSTRAINT PK_ats_job_interview_stages PRIMARY KEY ( id_ats_job_interview_stage ) ); + CREATE INDEX FK_ATS_Jobs_ATS_JobInterview_ID ON ats_job_interview_stages ( id_ats_job ); + COMMENT ON TABLE ats_job_interview_stages IS 'The JobInterviewStage object is used to represent a particular recruiting stage for an Application. A given Application typically has the JobInterviewStage object represented in the current_stage field.'; + COMMENT ON COLUMN ats_job_interview_stages.id_ats_job IS 'This field is populated only if the stage is specific to a particular job. If the stage is generic, this field will not be populated.'; + -- ************************************** ats_eeocs CREATE TABLE ats_eeocs ( @@ -1035,16 +1315,20 @@ CREATE TABLE ats_eeocs id_connection uuid NOT NULL, CONSTRAINT PK_ats_eeocs PRIMARY KEY ( id_ats_eeoc ) ); + CREATE INDEX FK_candidate_eeocsid ON ats_eeocs ( id_ats_candidate ); + COMMENT ON TABLE ats_eeocs IS 'The EEOC object is used to represent the Equal Employment Opportunity Commission information for a candidate (race, gender, veteran status, disability status).'; + COMMENT ON COLUMN ats_eeocs.race IS 'The candidate''s race. Possible values include: AMERICAN_INDIAN_OR_ALASKAN_NATIVE, ASIAN, BLACK_OR_AFRICAN_AMERICAN, HISPANIC_OR_LATINO, WHITE, NATIVE_HAWAIIAN_OR_OTHER_PACIFIC_ISLANDER, TWO_OR_MORE_RACES, DECLINE_TO_SELF_IDENTIFY. In cases where there is no clear mapping, the original value passed through will be returned.'; COMMENT ON COLUMN ats_eeocs.gender IS 'The candidate''s gender. Possible values include: MALE, FEMALE, NON-BINARY, OTHER, DECLINE_TO_SELF_IDENTIFY. In cases where there is no clear mapping, the original value passed through will be returned.'; COMMENT ON COLUMN ats_eeocs.veteran_status IS 'The candidate''s veteran status. Possible values include: I_AM_NOT_A_PROTECTED_VETERAN, I_IDENTIFY_AS_ONE_OR_MORE_OF_THE_CLASSIFICATIONS_OF_A_PROTECTED_VETERAN, I_DONT_WISH_TO_ANSWER. In cases where there is no clear mapping, the original value passed through will be returned.'; COMMENT ON COLUMN ats_eeocs.disability_status IS 'The candidate''s disability status. Possible values include: YES_I_HAVE_A_DISABILITY_OR_PREVIOUSLY_HAD_A_DISABILITY, NO_I_DONT_HAVE_A_DISABILITY, I_DONT_WISH_TO_ANSWER. In cases where there is no clear mapping, the original value passed through will be returned.'; + -- ************************************** ats_candidate_urls CREATE TABLE ats_candidate_urls ( @@ -1056,11 +1340,13 @@ CREATE TABLE ats_candidate_urls id_ats_candidate uuid NOT NULL, CONSTRAINT PK_ats_candidate_urls PRIMARY KEY ( id_ats_candidate_url ) ); + CREATE INDEX FK_candidate_url_ID ON ats_candidate_urls ( id_ats_candidate ); + -- ************************************** ats_candidate_phone_numbers CREATE TABLE ats_candidate_phone_numbers ( @@ -1072,12 +1358,15 @@ CREATE TABLE ats_candidate_phone_numbers id_ats_candidate uuid NOT NULL, CONSTRAINT PK_ats_candidate_phone_numbers PRIMARY KEY ( id_ats_candidate_phone_number ) ); + CREATE INDEX FK_candidate_phone_id ON ats_candidate_phone_numbers ( id_ats_candidate ); + COMMENT ON COLUMN ats_candidate_phone_numbers.type IS 'can be PERSONAL, PRO...'; + -- ************************************** ats_candidate_email_addresses CREATE TABLE ats_candidate_email_addresses ( @@ -1089,11 +1378,13 @@ CREATE TABLE ats_candidate_email_addresses id_ats_candidate uuid NOT NULL, CONSTRAINT PK_ats_candidate_email_addresses PRIMARY KEY ( id_ats_candidate_email_address ) ); + CREATE INDEX FK_candidate_email_ID ON ats_candidate_email_addresses ( id_ats_candidate ); + -- ************************************** ats_candidate_attachments CREATE TABLE ats_candidate_attachments ( @@ -1110,12 +1401,15 @@ CREATE TABLE ats_candidate_attachments id_connection uuid NOT NULL, CONSTRAINT PK_ats_candidate_attachments PRIMARY KEY ( id_ats_candidate_attachment ) ); + CREATE INDEX FK_ats_candidate_attachment_candidateID_Index ON ats_candidate_attachments ( id_ats_candidate ); + COMMENT ON COLUMN ats_candidate_attachments.file_type IS 'Can be RESUME, COVER_LETTER, OFFER_LETTER, OTHER'; + -- ************************************** ats_applications CREATE TABLE ats_applications ( @@ -1135,18 +1429,22 @@ CREATE TABLE ats_applications id_connection uuid NOT NULL, CONSTRAINT PK_ats_applications PRIMARY KEY ( id_ats_application ) ); + CREATE INDEX FK_ats_application_ATS_JOB_ID ON ats_applications ( id_ats_job ); + CREATE INDEX FK_ats_application_atsCandidateId ON ats_applications ( id_ats_candidate ); + COMMENT ON COLUMN ats_applications."source" IS 'the applications source'; COMMENT ON COLUMN ats_applications.credited_to IS 'The user credited for this application.'; COMMENT ON COLUMN ats_applications.current_stage IS 'this is an id_ats_job_interview_stage'; + -- ************************************** ats_activities CREATE TABLE ats_activities ( @@ -1163,16 +1461,19 @@ CREATE TABLE ats_activities id_connection uuid NOT NULL, CONSTRAINT PK_ats_activities PRIMARY KEY ( id_ats_activity ) ); + CREATE INDEX FK_activity_candidate ON ats_activities ( id_ats_candidate ); + COMMENT ON COLUMN ats_activities.activity_type IS 'The activity''s type. Possible values include: NOTE, EMAIL, OTHER. In cases where there is no clear mapping, the original value passed through will be returned.'; COMMENT ON COLUMN ats_activities.subject IS 'The activity''s subject.'; COMMENT ON COLUMN ats_activities.body IS 'The activity''s body.'; COMMENT ON COLUMN ats_activities.visibility IS 'The activity''s visibility. Possible values include: ADMIN_ONLY, PUBLIC, PRIVATE. In cases where there is no clear mapping, the original value passed through will be returned.'; COMMENT ON COLUMN ats_activities.id_ats_candidate IS 'The activity’s candidate.'; + -- ************************************** acc_transactions_lines_items CREATE TABLE acc_transactions_lines_items ( @@ -1194,12 +1495,15 @@ CREATE TABLE acc_transactions_lines_items id_acc_transaction uuid NULL, CONSTRAINT PK_acc_transactions_lines_items PRIMARY KEY ( id_acc_transactions_lines_item ) ); + CREATE INDEX FK_acc_transactions_lineItems ON acc_transactions_lines_items ( id_acc_transaction ); + COMMENT ON COLUMN acc_transactions_lines_items.id_acc_company_info IS 'The company the line belongs to.'; + -- ************************************** acc_purchase_orders CREATE TABLE acc_purchase_orders ( @@ -1226,10 +1530,12 @@ CREATE TABLE acc_purchase_orders id_acc_accounting_period uuid NULL, CONSTRAINT PK_acc_purchase_orders PRIMARY KEY ( id_acc_purchase_order ) ); + CREATE INDEX FK_purchaseOrder_accountingPeriod ON acc_purchase_orders ( id_acc_accounting_period ); + COMMENT ON COLUMN acc_purchase_orders.status IS 'The purchase order''s status. Possible values include: DRAFT, SUBMITTED, AUTHORIZED, BILLED, DELETED. In cases where there is no clear mapping, the original value passed through will be returned.'; COMMENT ON COLUMN acc_purchase_orders.delivery_address IS 'contains a id_acc_address'; COMMENT ON COLUMN acc_purchase_orders.customer IS 'The contact making the purchase order. @@ -1237,6 +1543,7 @@ Contains a id_acc_contact'; COMMENT ON COLUMN acc_purchase_orders.vendor IS 'contains a id_acc_contact'; COMMENT ON COLUMN acc_purchase_orders.id_acc_accounting_period IS 'The accounting period that the PurchaseOrder was generated in.'; + -- ************************************** acc_journal_entries CREATE TABLE acc_journal_entries ( @@ -1260,15 +1567,18 @@ CREATE TABLE acc_journal_entries modified_at timestamp with time zone NOT NULL, CONSTRAINT PK_acc_journal_entries PRIMARY KEY ( id_acc_journal_entry ) ); + CREATE INDEX FK_journal_entry_accounting_period ON acc_journal_entries ( id_acc_accounting_period ); + CREATE INDEX FK_journal_entry_companyInfo ON acc_journal_entries ( id_acc_company_info ); + -- ************************************** acc_contacts CREATE TABLE acc_contacts ( @@ -1288,12 +1598,15 @@ CREATE TABLE acc_contacts modified_at timestamp with time zone NOT NULL, CONSTRAINT PK_acc_contacts PRIMARY KEY ( id_acc_contact ) ); + CREATE INDEX FK_acc_contact_company ON acc_contacts ( id_acc_company_info ); + COMMENT ON COLUMN acc_contacts.status IS 'The contact''s status Possible values include: ACTIVE, ARCHIVED. In cases where there is no clear mapping, the original value passed through will be returned.'; + -- ************************************** acc_cash_flow_statement_report_items CREATE TABLE acc_cash_flow_statement_report_items ( @@ -1309,12 +1622,15 @@ CREATE TABLE acc_cash_flow_statement_report_items id_acc_cash_flow_statement uuid NULL, CONSTRAINT PK_acc_cash_flow_statement_report_items PRIMARY KEY ( id_acc_cash_flow_statement_report_item ) ); + CREATE INDEX FK_cashflow_statement_acc_cash_flow_statement_report_item ON acc_cash_flow_statement_report_items ( id_acc_cash_flow_statement ); + COMMENT ON COLUMN acc_cash_flow_statement_report_items.type IS 'can be operating, investing, financing'; + -- ************************************** acc_balance_sheets CREATE TABLE acc_balance_sheets ( @@ -1334,15 +1650,18 @@ CREATE TABLE acc_balance_sheets id_connection uuid NOT NULL, CONSTRAINT PK_acc_balance_sheets PRIMARY KEY ( id_acc_balance_sheet ) ); + CREATE INDEX FK_balanceSheetCompanyInfoID ON acc_balance_sheets ( id_acc_company_info ); + COMMENT ON COLUMN acc_balance_sheets.net_assets IS 'The balance sheet''s net assets.'; COMMENT ON COLUMN acc_balance_sheets.assets IS 'array of id_acc_balance_sheets_report_item objects'; COMMENT ON COLUMN acc_balance_sheets.liabilities IS 'array of id_acc_balance_sheets_report_item objects'; COMMENT ON COLUMN acc_balance_sheets.equity IS 'array of id_acc_balance_sheets_report_item objects'; + -- ************************************** acc_accounts CREATE TABLE acc_accounts ( @@ -1363,16 +1682,19 @@ CREATE TABLE acc_accounts id_connection uuid NOT NULL, CONSTRAINT PK_acc_accounts PRIMARY KEY ( id_acc_account ) ); + CREATE INDEX FK_accounts_companyinfo_ID ON acc_accounts ( id_acc_company_info ); + COMMENT ON COLUMN acc_accounts.classification IS 'The account''s broadest grouping. Possible values include: ASSET, EQUITY, EXPENSE, LIABILITY, REVENUE. In cases where there is no clear mapping, the original value passed through will be returned.'; COMMENT ON COLUMN acc_accounts.type IS 'The account''s type is a narrower and more specific grouping within the account''s classification.'; COMMENT ON COLUMN acc_accounts.status IS 'The account''s status. Possible values include: ACTIVE, PENDING, INACTIVE. In cases where there is no clear mapping, the original value passed through will be returned.'; COMMENT ON COLUMN acc_accounts.current_balance IS 'Use cents. 100 USD would be 10000'; COMMENT ON COLUMN acc_accounts.currency IS 'Possible values include: XUA, AFN, AFA, ALL, ALK, DZD, ADP, AOA, AOK, AON, AOR, ARA, ARS, ARM, ARP, ARL, AMD, AWG, AUD, ATS, AZN, AZM, BSD, BHD, BDT, BBD, BYN, BYB, BYR, BEF, BEC, BEL, BZD, BMD, BTN, BOB, BOL, BOV, BOP, BAM, BAD, BAN, BWP, BRC, BRZ, BRE, BRR, BRN, BRB, BRL, GBP, BND, BGL, BGN, BGO, BGM, BUK, BIF, XPF, KHR, CAD, CVE, KYD, XAF, CLE, CLP, CLF, CNX, CNY, CNH, COP, COU, KMF, CDF, CRC, HRD, HRK, CUC, CUP, CYP, CZK, CSK, DKK, DJF, DOP, NLG, XCD, DDM, ECS, ECV, EGP, GQE, ERN, EEK, ETB, EUR, XBA, XEU, XBB, XBC, XBD, FKP, FJD, FIM, FRF, XFO, XFU, GMD, GEK, GEL, DEM, GHS, GHC, GIP, XAU, GRD, GTQ, GWP, GNF, GNS, GYD, HTG, HNL, HKD, HUF, IMP, ISK, ISJ, INR, IDR, IRR, IQD, IEP, ILS, ILP, ILR, ITL, JMD, JPY, JOD, KZT, KES, KWD, KGS, LAK, LVL, LVR, LBP, LSL, LRD, LYD, LTL, LTT, LUL, LUC, LUF, MOP, MKD, MKN, MGA, MGF, MWK, MYR, MVR, MVP, MLF, MTL, MTP, MRU, MRO, MUR, MXV, MXN, MXP, MDC, MDL, MCF, MNT, MAD, MAF, MZE, MZN, MZM, MMK, NAD, NPR, ANG, TWD, NZD, NIO, NIC, NGN, KPW, NOK, OMR, PKR, XPD, PAB, PGK, PYG, PEI, PEN, PES, PHP, XPT, PLN, PLZ, PTE, GWE, QAR, XRE, RHD, RON, ROL, RUB, RUR, RWF, SVC, WST, SAR, RSD, CSD, SCR, SLL, XAG, SGD, SKK, SIT, SBD, SOS, ZAR, ZAL, KRH, KRW, KRO, SSP, SUR, ESP, ESA, ESB, XDR, LKR, SHP, XSU, SDD, SDG, SDP, SRD, SRG, SZL, SEK, CHF, SYP, STN, STD, TVD, TJR, TJS, TZS, XTS, THB, XXX, TPE, TOP, TTD, TND, TRY, TRL, TMT, TMM, USD, USN, USS, UGX, UGS, UAH, UAK, AED, UYW, UYU, UYP, UYI, UZS, VUV, VES, VEB, VEF, VND, VNN, CHE, CHW, XOF, YDD, YER, YUN, YUD, YUM, YUR, ZWN, ZRN, ZRZ, ZMW, ZMK, ZWD, ZWR, ZWL. In cases where there is no clear mapping, the original value passed through will be returned.'; + -- ************************************** value CREATE TABLE value ( @@ -1386,16 +1708,20 @@ CREATE TABLE value CONSTRAINT FK_33 FOREIGN KEY ( id_attribute ) REFERENCES attribute ( id_attribute ), CONSTRAINT FK_34 FOREIGN KEY ( id_entity ) REFERENCES entity ( id_entity ) ); + CREATE INDEX FK_value_attributeID ON value ( id_attribute ); + CREATE INDEX FK_value_entityID ON value ( id_entity ); + COMMENT ON COLUMN value.data IS 'can be: true, false, 0, 1 , 2 3 , 4 , hello, world ...'; + -- ************************************** tcg_tags CREATE TABLE tcg_tags ( @@ -1404,18 +1730,20 @@ CREATE TABLE tcg_tags remote_id text NULL, remote_platform text NULL, id_tcg_ticket uuid NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, id_linked_user uuid NULL, id_connection uuid NOT NULL, CONSTRAINT PK_tcg_tags PRIMARY KEY ( id_tcg_tag ), CONSTRAINT FK_48 FOREIGN KEY ( id_tcg_ticket ) REFERENCES tcg_tickets ( id_tcg_ticket ) ); + CREATE INDEX FK_tcg_tag_tcg_ticketID ON tcg_tags ( id_tcg_ticket ); + -- ************************************** tcg_comments CREATE TABLE tcg_comments ( @@ -1431,29 +1759,35 @@ CREATE TABLE tcg_comments id_tcg_contact uuid NULL, id_tcg_user uuid NULL, id_linked_user uuid NULL, - created_at timestamp NULL, - modified_at timestamp NULL, + created_at timestamp with time zone NULL, + modified_at timestamp with time zone NULL, id_connection uuid NOT NULL, CONSTRAINT PK_tcg_comments PRIMARY KEY ( id_tcg_comment ), CONSTRAINT FK_41 FOREIGN KEY ( id_tcg_contact ) REFERENCES tcg_contacts ( id_tcg_contact ), CONSTRAINT FK_40_1 FOREIGN KEY ( id_tcg_ticket ) REFERENCES tcg_tickets ( id_tcg_ticket ), CONSTRAINT FK_42 FOREIGN KEY ( id_tcg_user ) REFERENCES tcg_users ( id_tcg_user ) ); + CREATE INDEX FK_tcg_comment_tcg_contact ON tcg_comments ( id_tcg_contact ); + CREATE INDEX FK_tcg_comment_tcg_ticket ON tcg_comments ( id_tcg_ticket ); + CREATE INDEX FK_tcg_comment_tcg_userID ON tcg_comments ( id_tcg_user ); + COMMENT ON TABLE tcg_comments IS 'The tcg_comment object represents a comment on a ticket.'; + COMMENT ON COLUMN tcg_comments.creator_type IS 'Who created the comment. Can be a a id_tcg_contact or a id_tcg_user'; + -- ************************************** linked_users CREATE TABLE linked_users ( @@ -1464,13 +1798,207 @@ CREATE TABLE linked_users CONSTRAINT key_id_linked_users PRIMARY KEY ( id_linked_user ), CONSTRAINT FK_10 FOREIGN KEY ( id_project ) REFERENCES projects ( id_project ) ); + CREATE INDEX FK_proectID_linked_users ON linked_users ( id_project ); + COMMENT ON COLUMN linked_users.linked_user_origin_id IS 'id of the customer, in our customers own systems'; COMMENT ON COLUMN linked_users.alias IS 'human-readable alias, for UI (ex ACME company)'; +-- ************************************** hris_timesheet_entries +CREATE TABLE hris_timesheet_entries +( + id_hris_timesheet_entry uuid NOT NULL, + hours_worked bigint NULL, + start_time timestamp with time zone NULL, + end_time timestamp with time zone NULL, + id_hris_employee uuid NULL, + remote_id text NULL, + remote_created_at timestamp with time zone NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, + remote_was_deleted boolean NOT NULL, + id_connection uuid NOT NULL, + CONSTRAINT PK_hris_timesheet_entries PRIMARY KEY ( id_hris_timesheet_entry ), + CONSTRAINT FK_timesheet_entry_employee_Id FOREIGN KEY ( id_hris_employee ) REFERENCES hris_employees ( id_hris_employee ) +); +CREATE INDEX FKx_timesheet_entry_employee_Id ON hris_timesheet_entries +( + id_hris_employee +); + +-- ************************************** hris_time_off_balances +CREATE TABLE hris_time_off_balances +( + id_hris_time_off_balance uuid NOT NULL, + balance bigint NULL, + id_hris_employee uuid NULL, + used bigint NULL, + policy_type text NULL, + remote_id text NULL, + remote_created_at timestamp with time zone NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, + remote_was_deleted boolean NOT NULL, + id_connection uuid NOT NULL, + CONSTRAINT PK_hris_time_off_balances PRIMARY KEY ( id_hris_time_off_balance ), + CONSTRAINT FK_hris_timeoff_balance_hris_employee_ID FOREIGN KEY ( id_hris_employee ) REFERENCES hris_employees ( id_hris_employee ) +); +CREATE INDEX FKx_hris_timeoff_balance_hris_employee_ID ON hris_time_off_balances +( + id_hris_employee +); + +-- ************************************** hris_employments +CREATE TABLE hris_employments +( + id_hris_employment uuid NOT NULL, + job_title text NOT NULL, + pay_rate bigint NULL, + pay_period text NULL, + pay_frequency text NULL, + pay_currency text NULL, + flsa_status text NULL, + effective_date date NULL, + employment_type text NULL, + remote_id text NULL, + remote_created_at timestamp with time zone NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, + remote_was_deleted boolean NOT NULL, + id_connection uuid NOT NULL, + id_hris_pay_group uuid NULL, + id_hris_employee uuid NULL, + CONSTRAINT PK_hris_employments PRIMARY KEY ( id_hris_employment ), + CONSTRAINT FK_107 FOREIGN KEY ( id_hris_employee ) REFERENCES hris_employees ( id_hris_employee ), + CONSTRAINT FK_employments_pay_group_Id FOREIGN KEY ( id_hris_pay_group ) REFERENCES hris_pay_groups ( id_hris_pay_group ) +); +CREATE INDEX FK_2 ON hris_employments +( + id_hris_employee +); +CREATE INDEX FKx_employments_pay_group_Id ON hris_employments +( + id_hris_pay_group +); +COMMENT ON COLUMN hris_employments.pay_rate IS 'pay rate, in usd, in cents'; +COMMENT ON COLUMN hris_employments.pay_period IS 'The time period covered by this pay rate. Available options are: HOUR, DAY, WEEK, EVERY_TWO_WEEKS, SEMIMONTHLY, MONTH, QUARTER, EVERY_SIX_MONTHS, and YEAR. If there is no direct match, the original value provided will be returned as is.'; + +-- ************************************** hris_employee_payroll_runs +CREATE TABLE hris_employee_payroll_runs +( + id_hris_employee_payroll_run uuid NOT NULL, + id_hris_employee uuid NULL, + id_hris_payroll_run uuid NULL, + gross_pay bigint NULL, + net_pay bigint NULL, + start_date timestamp with time zone NULL, + end_date timestamp with time zone NULL, + check_date timestamp with time zone NULL, + remote_id text NULL, + remote_created_at timestamp with time zone NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, + remote_was_deleted boolean NOT NULL, + id_connection uuid NOT NULL, + CONSTRAINT PK_hris_employee_payroll_runs PRIMARY KEY ( id_hris_employee_payroll_run ), + CONSTRAINT FK_employee_payroll_run_payroll_run_Id FOREIGN KEY ( id_hris_payroll_run ) REFERENCES hris_payroll_runs ( id_hris_payroll_run ), + CONSTRAINT FK_hris_employee_payroll_run_employee_Id FOREIGN KEY ( id_hris_employee ) REFERENCES hris_employees ( id_hris_employee ) +); +CREATE INDEX FKx_employee_payroll_run_payroll_run_Id ON hris_employee_payroll_runs +( + id_hris_payroll_run +); +CREATE INDEX FKx_hris_employee_payroll_run_employee_Id ON hris_employee_payroll_runs +( + id_hris_employee +); + +-- ************************************** hris_dependents +CREATE TABLE hris_dependents +( + id_hris_dependents uuid NOT NULL, + first_name text NULL, + last_name text NULL, + middle_name text NULL, + relationship text NULL, + date_of_birth date NULL, + gender text NULL, + phone_number text NULL, + home_location uuid NULL, + is_student boolean NULL, + ssn text NULL, + id_hris_employee uuid NULL, + remote_id text NULL, + remote_created_at timestamp with time zone NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, + remote_was_deleted boolean NOT NULL, + id_connection uuid NOT NULL, + CONSTRAINT PK_hris_dependents PRIMARY KEY ( id_hris_dependents ), + CONSTRAINT FK_hris_dependant_hris_employee_Id FOREIGN KEY ( id_hris_employee ) REFERENCES hris_employees ( id_hris_employee ) +); +CREATE INDEX FKx_hris_dependant_hris_employee_Id ON hris_dependents +( + id_hris_employee +); +COMMENT ON COLUMN hris_dependents.home_location IS 'contains a id_hris_location'; + +-- ************************************** hris_benefits +CREATE TABLE hris_benefits +( + id_hris_benefit uuid NOT NULL, + remote_id text NULL, + remote_created_at timestamp with time zone NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, + remote_was_deleted boolean NOT NULL, + id_connection uuid NOT NULL, + provider_name text NULL, + id_hris_employee uuid NULL, + employee_contribution bigint NULL, + company_contribution bigint NULL, + start_date timestamp with time zone NULL, + end_date timestamp with time zone NULL, + id_hris_employer_benefit uuid NULL, + CONSTRAINT PK_hris_benefits PRIMARY KEY ( id_hris_benefit ), + CONSTRAINT FK_hris_benefit_employer_benefit_Id FOREIGN KEY ( id_hris_employer_benefit ) REFERENCES hris_employer_benefits ( id_hris_employer_benefit ), + CONSTRAINT FK_hris_benefits_employeeId FOREIGN KEY ( id_hris_employee ) REFERENCES hris_employees ( id_hris_employee ) +); +CREATE INDEX FKx_hris_benefit_employer_benefit_Id ON hris_benefits +( + id_hris_employer_benefit +); +CREATE INDEX FKx_hris_benefits_employeeId ON hris_benefits +( + id_hris_employee +); + +-- ************************************** hris_bank_infos +CREATE TABLE hris_bank_infos +( + id_hris_bank_info uuid NOT NULL, + account_type text NULL, + bank_name text NULL, + account_number text NULL, + routing_number text NULL, + remote_id text NULL, + remote_created_at timestamp with time zone NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, + remote_was_deleted boolean NOT NULL, + id_connection uuid NOT NULL, + id_hris_employee uuid NULL, + CONSTRAINT PK_hris_bank_infos PRIMARY KEY ( id_hris_bank_info ), + CONSTRAINT FK_bank_infos_employeeId FOREIGN KEY ( id_hris_employee ) REFERENCES hris_employees ( id_hris_employee ) +); +CREATE INDEX FKX_bank_infos_employeeId ON hris_bank_infos +( + id_hris_employee +); + -- ************************************** fs_files CREATE TABLE fs_files ( @@ -1482,20 +2010,23 @@ CREATE TABLE fs_files remote_id text NULL, id_fs_permission uuid NULL, id_fs_folder uuid NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, id_connection uuid NOT NULL, CONSTRAINT PK_fs_files PRIMARY KEY ( id_fs_file ) ); + CREATE INDEX FK_fs_file_FolderID ON fs_files ( id_fs_folder ); + CREATE INDEX FK_fs_file_permissionID ON fs_files ( id_fs_permission ); + -- ************************************** ecom_fulfilments CREATE TABLE ecom_fulfilments ( @@ -1513,12 +2044,15 @@ CREATE TABLE ecom_fulfilments CONSTRAINT PK_ecom_fulfilments PRIMARY KEY ( id_ecom_fulfilment ), CONSTRAINT FK_ecom_order_fulfilment FOREIGN KEY ( id_ecom_order ) REFERENCES ecom_orders ( id_ecom_order ) ); + CREATE INDEX FK_index_ecom_order_fulfilment ON ecom_fulfilments ( id_ecom_order ); + COMMENT ON COLUMN ecom_fulfilments.items IS 'array of ecom_products info'; + -- ************************************** ecom_addresses CREATE TABLE ecom_addresses ( @@ -1534,21 +2068,25 @@ CREATE TABLE ecom_addresses modified_at timestamp with time zone NOT NULL, created_at timestamp with time zone NOT NULL, remote_deleted boolean NOT NULL, - id_ecom_order uuid NULL, + id_ecom_order uuid NOT NULL, CONSTRAINT PK_ecom_customer_addresses PRIMARY KEY ( id_ecom_address ), CONSTRAINT FK_ecom_customer_customeraddress FOREIGN KEY ( id_ecom_customer ) REFERENCES ecom_customers ( id_ecom_customer ), CONSTRAINT FK_ecom_order_address FOREIGN KEY ( id_ecom_order ) REFERENCES ecom_orders ( id_ecom_order ) ); + CREATE INDEX FK_index_ecom_customer_customeraddress ON ecom_addresses ( id_ecom_customer ); + CREATE INDEX FK_index_FK_ecom_order_address ON ecom_addresses ( id_ecom_order ); + COMMENT ON COLUMN ecom_addresses.address_type IS 'billing, shipping, other'; + -- ************************************** crm_phone_numbers CREATE TABLE crm_phone_numbers ( @@ -1556,8 +2094,8 @@ CREATE TABLE crm_phone_numbers phone_number text NULL, phone_type text NULL, owner_type text NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, id_crm_company uuid NULL, id_crm_contact uuid NULL, id_connection uuid NOT NULL, @@ -1565,16 +2103,20 @@ CREATE TABLE crm_phone_numbers CONSTRAINT FK_phonenumber_crm_contactID FOREIGN KEY ( id_crm_contact ) REFERENCES crm_contacts ( id_crm_contact ), CONSTRAINT FK_17 FOREIGN KEY ( id_crm_company ) REFERENCES crm_companies ( id_crm_company ) ); + CREATE INDEX crm_contactID_crm_contact_phone_number ON crm_phone_numbers ( id_crm_contact ); + CREATE INDEX FK_phone_number_companyID ON crm_phone_numbers ( id_crm_company ); + COMMENT ON COLUMN crm_phone_numbers.owner_type IS 'can be ''COMPANY'' or ''CONTACT'' - helps locate who to link the phone number to.'; + -- ************************************** crm_engagements CREATE TABLE crm_engagements ( @@ -1583,8 +2125,8 @@ CREATE TABLE crm_engagements type text NULL, direction text NULL, subject text NULL, - start_at timestamp NULL, - end_time timestamp NULL, + start_at timestamp with time zone NULL, + end_time timestamp with time zone NULL, remote_id text NULL, id_linked_user uuid NULL, remote_platform text NULL, @@ -1592,24 +2134,29 @@ CREATE TABLE crm_engagements id_crm_user uuid NULL, id_connection uuid NOT NULL, contacts text[] NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, CONSTRAINT PK_crm_engagement PRIMARY KEY ( id_crm_engagement ), CONSTRAINT FK_crm_engagement_crm_user FOREIGN KEY ( id_crm_user ) REFERENCES crm_users ( id_crm_user ), CONSTRAINT FK_29 FOREIGN KEY ( id_crm_company ) REFERENCES crm_companies ( id_crm_company ) ); + CREATE INDEX FK_crm_engagement_crm_user_ID ON crm_engagements ( id_crm_user ); + CREATE INDEX FK_crm_engagement_crmCompanyID ON crm_engagements ( id_crm_company ); + COMMENT ON COLUMN crm_engagements.type IS 'can be (but not restricted to) + MEETING, CALL, EMAIL'; COMMENT ON COLUMN crm_engagements.contacts IS 'array of id_crm_contact (uuids)'; + -- ************************************** crm_email_addresses CREATE TABLE crm_email_addresses ( @@ -1617,8 +2164,8 @@ CREATE TABLE crm_email_addresses email_address text NOT NULL, email_address_type text NOT NULL, owner_type text NOT NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, id_crm_company uuid NULL, id_crm_contact uuid NULL, id_connection uuid NOT NULL, @@ -1626,16 +2173,20 @@ CREATE TABLE crm_email_addresses CONSTRAINT FK_3 FOREIGN KEY ( id_crm_contact ) REFERENCES crm_contacts ( id_crm_contact ), CONSTRAINT FK_16 FOREIGN KEY ( id_crm_company ) REFERENCES crm_companies ( id_crm_company ) ); + CREATE INDEX crm_contactID_crm_contact_email_address ON crm_email_addresses ( id_crm_contact ); + CREATE INDEX FK_contact_email_adress_companyID ON crm_email_addresses ( id_crm_company ); + COMMENT ON COLUMN crm_email_addresses.owner_type IS 'can be ''COMPANY'' or ''CONTACT'' - helps locate who to link the email belongs to.'; + -- ************************************** crm_deals CREATE TABLE crm_deals ( @@ -1643,8 +2194,8 @@ CREATE TABLE crm_deals name text NOT NULL, description text NULL, amount bigint NOT NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, remote_id text NULL, remote_platform text NULL, id_crm_user uuid NULL, @@ -1657,20 +2208,25 @@ CREATE TABLE crm_deals CONSTRAINT FK_21 FOREIGN KEY ( id_crm_deals_stage ) REFERENCES crm_deals_stages ( id_crm_deals_stage ), CONSTRAINT FK_47_1 FOREIGN KEY ( id_crm_company ) REFERENCES crm_companies ( id_crm_company ) ); + CREATE INDEX crm_deal_crm_userID ON crm_deals ( id_crm_user ); + CREATE INDEX crm_deal_deal_stageID ON crm_deals ( id_crm_deals_stage ); + CREATE INDEX FK_crm_deal_crmCompanyID ON crm_deals ( id_crm_company ); + COMMENT ON COLUMN crm_deals.amount IS 'AMOUNT IN CENTS'; + -- ************************************** crm_addresses CREATE TABLE crm_addresses ( @@ -1685,25 +2241,30 @@ CREATE TABLE crm_addresses id_crm_company uuid NULL, id_crm_contact uuid NULL, id_connection uuid NOT NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, owner_type text NOT NULL, CONSTRAINT PK_crm_addresses PRIMARY KEY ( id_crm_address ), CONSTRAINT FK_14 FOREIGN KEY ( id_crm_contact ) REFERENCES crm_contacts ( id_crm_contact ), CONSTRAINT FK_15 FOREIGN KEY ( id_crm_company ) REFERENCES crm_companies ( id_crm_company ) ); + CREATE INDEX FK_crm_addresses_to_crm_contacts ON crm_addresses ( id_crm_contact ); + CREATE INDEX FK_crm_adresses_to_crm_companies ON crm_addresses ( id_crm_company ); + COMMENT ON COLUMN crm_addresses.owner_type IS 'Can be a company or a contact''s address + ''company'' ''contact'''; + -- ************************************** ats_offers CREATE TABLE ats_offers ( @@ -1721,13 +2282,16 @@ CREATE TABLE ats_offers id_connection uuid NOT NULL, CONSTRAINT PK_ats_offers PRIMARY KEY ( id_ats_offer ) ); + CREATE INDEX FK_ats_offers_applicationID ON ats_offers ( id_ats_application ); + COMMENT ON COLUMN ats_offers.created_by IS 'the ats_user who created this ID'; COMMENT ON COLUMN ats_offers.status IS 'The offer''s status. Possible values include: DRAFT, APPROVAL-SENT, APPROVED, SENT, SENT-MANUALLY, OPENED, DENIED, SIGNED, DEPRECATED. In cases where there is no clear mapping, the original value passed through will be returned.'; + -- ************************************** ats_interviews CREATE TABLE ats_interviews ( @@ -1748,19 +2312,23 @@ CREATE TABLE ats_interviews id_connection uuid NOT NULL, CONSTRAINT PK_ats_interviews PRIMARY KEY ( id_ats_interview ) ); + CREATE INDEX FK_applications_interviews ON ats_interviews ( id_ats_application ); + CREATE INDEX FK_id_ats_job_interview_stageID ON ats_interviews ( id_ats_job_interview_stage ); + COMMENT ON COLUMN ats_interviews.status IS 'The interview''s status. Possible values include: SCHEDULED, AWAITING_FEEDBACK, COMPLETE. In cases where there is no clear mapping, the original value passed through will be returned.'; COMMENT ON COLUMN ats_interviews.organized_by IS 'The user organizing the interview. Data is a id_ats_user.'; COMMENT ON COLUMN ats_interviews.interviewers IS 'Array of RemoteUser IDs.'; COMMENT ON COLUMN ats_interviews.id_ats_job_interview_stage IS 'The stage of the interview.'; + -- ************************************** api_keys CREATE TABLE api_keys ( @@ -1771,17 +2339,17 @@ CREATE TABLE api_keys id_user uuid NOT NULL, CONSTRAINT id_ PRIMARY KEY ( id_api_key ), CONSTRAINT unique_api_keys UNIQUE ( api_key_hash ), - CONSTRAINT FK_8 FOREIGN KEY ( id_user ) REFERENCES users ( id_user ), - CONSTRAINT FK_7 FOREIGN KEY ( id_project ) REFERENCES projects ( id_project ) -); -CREATE INDEX FK_2 ON api_keys -( - id_user + CONSTRAINT FK_api_key_project_Id FOREIGN KEY ( id_project ) REFERENCES projects ( id_project ), + CONSTRAINT FK_api_keys_user_Id FOREIGN KEY ( id_user ) REFERENCES users ( id_user ) ); CREATE INDEX FK_api_keys_projects ON api_keys ( id_project ); +CREATE INDEX FKx_api_keys_user_Id ON api_keys +( + id_user +); -- ************************************** acc_purchase_orders_line_items CREATE TABLE acc_purchase_orders_line_items @@ -1803,17 +2371,20 @@ CREATE TABLE acc_purchase_orders_line_items id_acc_company uuid NULL, CONSTRAINT PK_acc_purchase_orders_line_items PRIMARY KEY ( id_acc_purchase_orders_line_item ) ); + CREATE INDEX FK_purchaseorder_purchaseorderLineItems ON acc_purchase_orders_line_items ( id_acc_purchase_order ); + -- ************************************** acc_phone_numbers CREATE TABLE acc_phone_numbers ( id_acc_phone_number uuid NOT NULL, "number" text NULL, type text NULL, + remote_id text NULL, created_at timestamp with time zone NOT NULL, modified_at timestamp with time zone NOT NULL, id_acc_company_info uuid NULL, @@ -1821,17 +2392,21 @@ CREATE TABLE acc_phone_numbers id_connection uuid NOT NULL, CONSTRAINT PK_acc_phone_numbers PRIMARY KEY ( id_acc_phone_number ) ); + CREATE INDEX FK_acc_phone_number_contact ON acc_phone_numbers ( id_acc_contact ); + CREATE INDEX FK_company_infos_phone_number ON acc_phone_numbers ( id_acc_company_info ); + COMMENT ON COLUMN acc_phone_numbers.id_acc_company_info IS 'holds a valueif if the phone number belongs to a acc_company_infos objects'; COMMENT ON COLUMN acc_phone_numbers.id_acc_contact IS 'holds a valueif if the phone number belongs to a acc_contact object'; + -- ************************************** acc_journal_entries_lines CREATE TABLE acc_journal_entries_lines ( @@ -1849,11 +2424,13 @@ CREATE TABLE acc_journal_entries_lines id_acc_journal_entry uuid NOT NULL, CONSTRAINT PK_acc_journal_entries_lines PRIMARY KEY ( id_acc_journal_entries_line ) ); + CREATE INDEX FK_journal_entries_entries_lines ON acc_journal_entries_lines ( id_acc_journal_entry ); + -- ************************************** acc_items CREATE TABLE acc_items ( @@ -1872,20 +2449,25 @@ CREATE TABLE acc_items id_connection uuid NOT NULL, CONSTRAINT PK_acc_items PRIMARY KEY ( id_acc_item ) ); + CREATE INDEX FK_acc_item_acc_account ON acc_items ( purchase_account ); + CREATE INDEX FK_acc_item_acc_company_infos ON acc_items ( id_acc_company_info ); + CREATE INDEX FK_acc_items_sales_account ON acc_items ( sales_account ); + COMMENT ON COLUMN acc_items.status IS 'The item''s status. Possible values include: ACTIVE, ARCHIVED. In cases where there is no clear mapping, the original value passed through will be returned.'; + -- ************************************** acc_invoices CREATE TABLE acc_invoices ( @@ -1914,18 +2496,22 @@ CREATE TABLE acc_invoices tracking_categories text[] NULL, CONSTRAINT PK_acc_invoices PRIMARY KEY ( id_acc_invoice ) ); + CREATE INDEX FK_acc_invoice_accounting_period_index ON acc_invoices ( id_acc_accounting_period ); + CREATE INDEX FK_invoice_contactID ON acc_invoices ( id_acc_contact ); + COMMENT ON COLUMN acc_invoices.type IS 'Whether the invoice is an accounts receivable or accounts payable. If type is ACCOUNTS_PAYABLE, the invoice is a bill. If type is ACCOUNTS_RECEIVABLE, it is an invoice. Possible values include: ACCOUNTS_RECEIVABLE, ACCOUNTS_PAYABLE. In cases where there is no clear mapping, the original value passed through will be returned.'; COMMENT ON COLUMN acc_invoices.total_discount IS 'The total discounts applied to the total cost.'; COMMENT ON COLUMN acc_invoices.status IS 'The status of the invoice. Possible values include: PAID, DRAFT, SUBMITTED, PARTIALLY_PAID, OPEN, VOID. In cases where there is no clear mapping, the original value passed through will be returned.'; + -- ************************************** acc_expenses CREATE TABLE acc_expenses ( @@ -1948,22 +2534,27 @@ CREATE TABLE acc_expenses tracking_categories text[] NULL, CONSTRAINT PK_acc_expenses PRIMARY KEY ( id_acc_expense ) ); + CREATE INDEX FK_acc_account_acc_expense_index ON acc_expenses ( id_acc_account ); + CREATE INDEX FK_acc_expense_acc_company_index ON acc_expenses ( id_acc_company_info ); + CREATE INDEX FK_acc_expense_acc_contact_index ON acc_expenses ( id_acc_contact ); + COMMENT ON COLUMN acc_expenses.transaction_date IS 'When the transaction occurred.'; COMMENT ON COLUMN acc_expenses.remote_created_at IS 'When the expense was created.'; COMMENT ON COLUMN acc_expenses.tracking_categories IS 'array of id_acc_tracking_category'; + -- ************************************** acc_attachments CREATE TABLE acc_attachments ( @@ -1977,11 +2568,13 @@ CREATE TABLE acc_attachments id_connection uuid NOT NULL, CONSTRAINT PK_acc_attachments PRIMARY KEY ( id_acc_attachment ) ); + CREATE INDEX FK_acc_attachments_accountID ON acc_attachments ( id_acc_account ); + -- ************************************** acc_addresses CREATE TABLE acc_addresses ( @@ -1990,6 +2583,7 @@ CREATE TABLE acc_addresses street_1 text NULL, street_2 text NULL, city text NULL, + remote_id text NULL, "state" text NULL, country_subdivision text NULL, country text NULL, @@ -2001,21 +2595,26 @@ CREATE TABLE acc_addresses id_connection uuid NOT NULL, CONSTRAINT PK_acc_addresses PRIMARY KEY ( id_acc_address ) ); + CREATE INDEX FK_acc_company_info_acc_adresses ON acc_addresses ( id_acc_company_info ); + CREATE INDEX FK_acc_contact_acc_addresses ON acc_addresses ( id_acc_contact ); + COMMENT ON TABLE acc_addresses IS 'The Address object is used to represent a contact''s or company''s address.'; + COMMENT ON COLUMN acc_addresses.type IS 'can be SHIPPING, BILLING, OFFICES, PO....'; COMMENT ON COLUMN acc_addresses."state" IS 'can also be a region'; COMMENT ON COLUMN acc_addresses.country_subdivision IS 'Also called a "departement" in some countries (ex: france)'; COMMENT ON COLUMN acc_addresses.id_acc_contact IS 'contains a value if the acc_address belongs to an acc_contact object'; COMMENT ON COLUMN acc_addresses.id_acc_company_info IS 'contains a value if the acc_address belongs to an acc_company_info object'; + -- ************************************** tcg_attachments CREATE TABLE tcg_attachments ( @@ -2025,8 +2624,8 @@ CREATE TABLE tcg_attachments file_name text NULL, file_url text NULL, uploader uuid NOT NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, id_linked_user uuid NULL, id_tcg_ticket uuid NULL, id_tcg_comment uuid NULL, @@ -2035,33 +2634,96 @@ CREATE TABLE tcg_attachments CONSTRAINT FK_51 FOREIGN KEY ( id_tcg_comment ) REFERENCES tcg_comments ( id_tcg_comment ), CONSTRAINT FK_50 FOREIGN KEY ( id_tcg_ticket ) REFERENCES tcg_tickets ( id_tcg_ticket ) ); + CREATE INDEX FK_tcg_attachment_tcg_commentID ON tcg_attachments ( id_tcg_comment ); + CREATE INDEX FK_tcg_attachment_tcg_ticketID ON tcg_attachments ( id_tcg_ticket ); + COMMENT ON COLUMN tcg_attachments.remote_id IS 'If empty, means the file is stored is panora but not in the destination platform (often because the platform doesn''t support )'; COMMENT ON COLUMN tcg_attachments.uploader IS 'id_tcg_user who uploaded the file'; COMMENT ON COLUMN tcg_attachments.id_tcg_ticket IS 'For cases where the ticketing platform does not specify which comment the attachment belongs to.'; + -- ************************************** invite_links CREATE TABLE invite_links ( - id_invite_link uuid NOT NULL, - status text NOT NULL, - email text NULL, - id_linked_user uuid NOT NULL, + id_invite_link uuid NOT NULL, + status text NOT NULL, + email text NULL, + id_linked_user uuid NOT NULL, + displayed_verticals text[] NULL, + displayed_providers text[] NULL, CONSTRAINT PK_invite_links PRIMARY KEY ( id_invite_link ), CONSTRAINT FK_37 FOREIGN KEY ( id_linked_user ) REFERENCES linked_users ( id_linked_user ) ); + CREATE INDEX FK_invite_link_linkedUserID ON invite_links ( id_linked_user ); +-- ************************************** hris_employee_payroll_runs_taxes +CREATE TABLE hris_employee_payroll_runs_taxes +( + id_hris_employee_payroll_runs_tax uuid NOT NULL, + name text NULL, + amount bigint NULL, + employer_tax boolean NULL, + id_hris_employee_payroll_run uuid NULL, + remote_id text NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, + CONSTRAINT PK_hris_employee_payroll_runs_taxes PRIMARY KEY ( id_hris_employee_payroll_runs_tax ), + CONSTRAINT FK_hris_employee_payroll_run_tax_hris_employee_payroll_run_id FOREIGN KEY ( id_hris_employee_payroll_run ) REFERENCES hris_employee_payroll_runs ( id_hris_employee_payroll_run ) +); +CREATE INDEX FKx_hris_employee_payroll_run_tax_hris_employee_payroll_run_id ON hris_employee_payroll_runs_taxes +( + id_hris_employee_payroll_run +); + +-- ************************************** hris_employee_payroll_runs_earnings +CREATE TABLE hris_employee_payroll_runs_earnings +( + id_hris_employee_payroll_runs_earning uuid NOT NULL, + amount bigint NULL, + type text NULL, + id_hris_employee_payroll_run uuid NULL, + remote_id text NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, + CONSTRAINT PK_hris_employee_payroll_runs_earnings PRIMARY KEY ( id_hris_employee_payroll_runs_earning ), + CONSTRAINT FK_hris_employee_payroll_runs_earning_hris_employee_payroll_run_Id FOREIGN KEY ( id_hris_employee_payroll_run ) REFERENCES hris_employee_payroll_runs ( id_hris_employee_payroll_run ) +); +CREATE INDEX FKx_hris_employee_payroll_runs_earning_hris_employee_payroll_run_Id ON hris_employee_payroll_runs_earnings +( + id_hris_employee_payroll_run +); + +-- ************************************** hris_employee_payroll_runs_deductions +CREATE TABLE hris_employee_payroll_runs_deductions +( + id_hris_employee_payroll_runs_deduction uuid NOT NULL, + remote_id text NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, + id_hris_employee_payroll_run uuid NULL, + name text NULL, + employee_deduction bigint NULL, + company_deduction bigint NULL, + CONSTRAINT PK_hris_employee_payroll_runs_deductions PRIMARY KEY ( id_hris_employee_payroll_runs_deduction ), + CONSTRAINT FK_hris_employee_payroll_runs_deduction_hris_employee_payroll_Id FOREIGN KEY ( id_hris_employee_payroll_run ) REFERENCES hris_employee_payroll_runs ( id_hris_employee_payroll_run ) +); +CREATE INDEX FKx_hris_employee_payroll_runs_deduction_hris_employee_payroll_Id ON hris_employee_payroll_runs_deductions +( + id_hris_employee_payroll_run +); + -- ************************************** events CREATE TABLE events ( @@ -2074,18 +2736,21 @@ CREATE TABLE events method text NOT NULL, url text NOT NULL, provider text NOT NULL, - "timestamp" timestamp NOT NULL DEFAULT NOW(), + "timestamp" timestamp with time zone NOT NULL DEFAULT NOW(), id_linked_user uuid NOT NULL, CONSTRAINT PK_jobs PRIMARY KEY ( id_event ), CONSTRAINT FK_12 FOREIGN KEY ( id_linked_user ) REFERENCES linked_users ( id_linked_user ) ); + CREATE INDEX FK_linkeduserID_projectID ON events ( id_linked_user ); + COMMENT ON COLUMN events.type IS 'example crm_contact.created crm_contact.deleted'; COMMENT ON COLUMN events.status IS 'pending,, retry_scheduled, failed, success'; + -- ************************************** crm_tasks CREATE TABLE crm_tasks ( @@ -2093,10 +2758,10 @@ CREATE TABLE crm_tasks subject text NULL, content text NULL, status text NULL, - due_date timestamp NULL, - finished_date timestamp NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + due_date timestamp with time zone NULL, + finished_date timestamp with time zone NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, id_crm_user uuid NULL, id_crm_company uuid NULL, id_crm_deal uuid NULL, @@ -2109,26 +2774,30 @@ CREATE TABLE crm_tasks CONSTRAINT FK_25 FOREIGN KEY ( id_crm_user ) REFERENCES crm_users ( id_crm_user ), CONSTRAINT FK_27 FOREIGN KEY ( id_crm_deal ) REFERENCES crm_deals ( id_crm_deal ) ); + CREATE INDEX FK_crm_task_companyID ON crm_tasks ( id_crm_company ); + CREATE INDEX FK_crm_task_userID ON crm_tasks ( id_crm_user ); + CREATE INDEX FK_crmtask_dealID ON crm_tasks ( id_crm_deal ); + -- ************************************** crm_notes CREATE TABLE crm_notes ( id_crm_note uuid NOT NULL, content text NOT NULL, - created_at timestamp NOT NULL, - modified_at timestamp NOT NULL, + created_at timestamp with time zone NOT NULL, + modified_at timestamp with time zone NOT NULL, id_crm_company uuid NULL, id_crm_contact uuid NULL, id_crm_deal uuid NULL, @@ -2142,23 +2811,28 @@ CREATE TABLE crm_notes CONSTRAINT FK_18 FOREIGN KEY ( id_crm_company ) REFERENCES crm_companies ( id_crm_company ), CONSTRAINT FK_20 FOREIGN KEY ( id_crm_deal ) REFERENCES crm_deals ( id_crm_deal ) ); + CREATE INDEX FK_crm_note_crm_companyID ON crm_notes ( id_crm_contact ); + CREATE INDEX FK_crm_note_crm_contactID ON crm_notes ( id_crm_company ); + CREATE INDEX FK_crm_note_crm_userID ON crm_notes ( id_crm_user ); + CREATE INDEX FK_crm_notes_crm_dealID ON crm_notes ( id_crm_deal ); + -- ************************************** connections CREATE TABLE connections ( @@ -2170,8 +2844,8 @@ CREATE TABLE connections token_type text NOT NULL, access_token text NULL, refresh_token text NULL, - expiration_timestamp timestamp NULL, - created_at timestamp NOT NULL, + expiration_timestamp timestamp with time zone NULL, + created_at timestamp with time zone NOT NULL, connection_token text NULL, id_project uuid NOT NULL, id_linked_user uuid NOT NULL, @@ -2179,18 +2853,22 @@ CREATE TABLE connections CONSTRAINT FK_9 FOREIGN KEY ( id_project ) REFERENCES projects ( id_project ), CONSTRAINT FK_11 FOREIGN KEY ( id_linked_user ) REFERENCES linked_users ( id_linked_user ) ); + CREATE INDEX FK_1 ON connections ( id_project ); + CREATE INDEX FK_connections_to_LinkedUsersID ON connections ( id_linked_user ); + COMMENT ON COLUMN connections.status IS 'ONLY FOR INVITE LINK'; COMMENT ON COLUMN connections.token_type IS 'The type of the token, such as "Bearer," "JWT," or any other supported type.'; COMMENT ON COLUMN connections.connection_token IS 'Connection token users will put in their header to identify which service / linked_User they make request for'; + -- ************************************** ats_scorecards CREATE TABLE ats_scorecards ( @@ -2206,18 +2884,22 @@ CREATE TABLE ats_scorecards id_connection uuid NOT NULL, CONSTRAINT PK_ats_scorecards PRIMARY KEY ( id_ats_scorecard ) ); + CREATE INDEX FK_applications_scorecard ON ats_scorecards ( id_ats_application ); + CREATE INDEX FK_interviews_scorecards ON ats_scorecards ( id_ats_interview ); + COMMENT ON COLUMN ats_scorecards.overall_recommendation IS 'The inteviewer''s recommendation. Possible values include: DEFINITELY_NO, NO, YES, STRONG_YES, NO_DECISION. In cases where there is no clear mapping, the original value passed through will be returned.'; COMMENT ON COLUMN ats_scorecards.id_ats_application IS 'The application being scored.'; COMMENT ON COLUMN ats_scorecards.id_ats_interview IS 'The interview being scored.'; + -- ************************************** acc_payments CREATE TABLE acc_payments ( @@ -2229,6 +2911,7 @@ CREATE TABLE acc_payments currency text NULL, exchange_rate text NULL, total_amount bigint NULL, + remote_id text NULL, type text NULL, remote_updated_at timestamp with time zone NULL, id_acc_company_info uuid NULL, @@ -2239,31 +2922,38 @@ CREATE TABLE acc_payments tracking_categories text[] NULL, CONSTRAINT PK_acc_payments PRIMARY KEY ( id_acc_payment ) ); + CREATE INDEX FK_acc_payment_acc_account_index ON acc_payments ( id_acc_account ); + CREATE INDEX FK_acc_payment_acc_company_index ON acc_payments ( id_acc_company_info ); + CREATE INDEX FK_acc_payment_acc_contact ON acc_payments ( id_acc_contact ); + CREATE INDEX FK_acc_payment_accounting_period_index ON acc_payments ( id_acc_accounting_period ); + CREATE INDEX FK_acc_payment_invoiceID ON acc_payments ( id_acc_invoice ); + COMMENT ON COLUMN acc_payments.id_acc_contact IS 'The supplier, or customer involved in the payment.'; COMMENT ON COLUMN acc_payments.id_acc_account IS 'The supplier’s or customer’s account in which the payment is made.'; COMMENT ON COLUMN acc_payments.type IS 'The type of the invoice. Possible values include: ACCOUNTS_PAYABLE, ACCOUNTS_RECEIVABLE. In cases where there is no clear mapping, the original value passed through will be returned.'; COMMENT ON COLUMN acc_payments.id_acc_company_info IS 'The company the payment belongs to.'; + -- ************************************** acc_invoices_line_items CREATE TABLE acc_invoices_line_items ( @@ -2283,15 +2973,18 @@ CREATE TABLE acc_invoices_line_items acc_tracking_categories text[] NULL, CONSTRAINT PK_acc_invoices_line_items PRIMARY KEY ( id_acc_invoices_line_item ) ); + CREATE INDEX FK_acc_invoice_line_items_index ON acc_invoices_line_items ( id_acc_invoice ); + CREATE INDEX FK_acc_items_lines_invoice_index ON acc_invoices_line_items ( id_acc_item ); + -- ************************************** acc_expense_lines CREATE TABLE acc_expense_lines ( @@ -2307,18 +3000,20 @@ CREATE TABLE acc_expense_lines id_connection uuid NOT NULL, CONSTRAINT PK_acc_expense_lines PRIMARY KEY ( id_acc_expense_line ) ); + CREATE INDEX FK_acc_expense_expense_lines_index ON acc_expense_lines ( id_acc_expense ); + -- ************************************** webhook_delivery_attempts CREATE TABLE webhook_delivery_attempts ( id_webhook_delivery_attempt uuid NOT NULL, - "timestamp" timestamp NOT NULL, + "timestamp" timestamp with time zone NOT NULL, status text NOT NULL, - next_retry timestamp NULL, + next_retry timestamp with time zone NULL, attempt_count bigint NOT NULL, id_webhooks_payload uuid NULL, id_webhook_endpoint uuid NULL, @@ -2330,47 +3025,58 @@ CREATE TABLE webhook_delivery_attempts CONSTRAINT FK_39 FOREIGN KEY ( id_event ) REFERENCES events ( id_event ), CONSTRAINT FK_40 FOREIGN KEY ( id_webhooks_reponse ) REFERENCES webhooks_reponses ( id_webhooks_reponse ) ); + CREATE INDEX FK_we_payload_webhookID ON webhook_delivery_attempts ( id_webhooks_payload ); + CREATE INDEX FK_we_webhookEndpointID ON webhook_delivery_attempts ( id_webhook_endpoint ); + CREATE INDEX FK_webhook_delivery_attempt_eventID ON webhook_delivery_attempts ( id_event ); + CREATE INDEX FK_webhook_delivery_attempt_webhook_responseID ON webhook_delivery_attempts ( id_webhooks_reponse ); + COMMENT ON COLUMN webhook_delivery_attempts."timestamp" IS 'timestamp of the delivery attempt'; COMMENT ON COLUMN webhook_delivery_attempts.status IS 'status of the delivery attempt + can be success, retry, failure'; COMMENT ON COLUMN webhook_delivery_attempts.next_retry IS 'if null no next retry'; COMMENT ON COLUMN webhook_delivery_attempts.attempt_count IS 'Number of attempt + can be 0 1 2 3 4 5 6'; + -- ************************************** jobs_status_history CREATE TABLE jobs_status_history ( id_jobs_status_history uuid NOT NULL, - "timestamp" timestamp NOT NULL DEFAULT NOW(), + "timestamp" timestamp with time zone NOT NULL DEFAULT NOW(), previous_status text NOT NULL, new_status text NOT NULL, id_event uuid NOT NULL, CONSTRAINT PK_jobs_status_history PRIMARY KEY ( id_jobs_status_history ), CONSTRAINT FK_4 FOREIGN KEY ( id_event ) REFERENCES events ( id_event ) ); + CREATE INDEX id_job_jobs_status_history ON jobs_status_history ( id_event ); + COMMENT ON COLUMN jobs_status_history.previous_status IS 'void when first initialization'; COMMENT ON COLUMN jobs_status_history.new_status IS 'pending, retry_scheduled, failed, success'; + -- ************************************** acc_payments_line_items CREATE TABLE acc_payments_line_items ( @@ -2386,9 +3092,12 @@ CREATE TABLE acc_payments_line_items id_connection uuid NOT NULL, CONSTRAINT PK_acc_payments_line_items PRIMARY KEY ( acc_payments_line_item ) ); + CREATE INDEX FK_acc_payment_line_items_index ON acc_payments_line_items ( id_acc_payment ); + COMMENT ON COLUMN acc_payments_line_items.related_object_type IS 'can either be a Invoice, CreditNote, or JournalEntry'; + diff --git a/packages/api/src/@core/@core-services/request-retry/retry.handler.ts b/packages/api/src/@core/@core-services/request-retry/retry.handler.ts index 3586e4b0b..0598f3115 100644 --- a/packages/api/src/@core/@core-services/request-retry/retry.handler.ts +++ b/packages/api/src/@core/@core-services/request-retry/retry.handler.ts @@ -17,7 +17,13 @@ export class RetryHandler { ): Promise { try { const response: AxiosResponse = await axios(config); - return response; + const responseInfo = { + status: response.status, + statusText: response.statusText, + headers: response.headers, + data: response.data, + }; + return responseInfo; } catch (error) { if (this.isRateLimitError(error)) { const retryId = uuidv4(); diff --git a/packages/api/src/@core/connections/connections.controller.ts b/packages/api/src/@core/connections/connections.controller.ts index fb62fe273..c91eb7818 100644 --- a/packages/api/src/@core/connections/connections.controller.ts +++ b/packages/api/src/@core/connections/connections.controller.ts @@ -1,13 +1,4 @@ -import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; -import { CategoryConnectionRegistry } from '@@core/@core-services/registries/connections-categories.registry'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; -import { JwtAuthGuard } from '@@core/auth/guards/jwt-auth.guard'; -import { CoreSyncService } from '@@core/sync/sync.service'; -import { ApiGetArrayCustomResponse } from '@@core/utils/dtos/openapi.respone.dto'; -import { ConnectionsError } from '@@core/utils/errors'; import { - Body, Controller, Get, Post, @@ -15,18 +6,27 @@ import { Request, Res, UseGuards, + Body, } from '@nestjs/common'; import { - ApiBody, - ApiExcludeController, - ApiExcludeEndpoint, + ApiTags, ApiOperation, ApiQuery, ApiResponse, - ApiTags, + ApiBody, + ApiExcludeEndpoint, } from '@nestjs/swagger'; -import { AuthStrategy, CONNECTORS_METADATA } from '@panora/shared'; import { Response } from 'express'; + +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { CategoryConnectionRegistry } from '@@core/@core-services/registries/connections-categories.registry'; +import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { JwtAuthGuard } from '@@core/auth/guards/jwt-auth.guard'; +import { CoreSyncService } from '@@core/sync/sync.service'; +import { ApiGetArrayCustomResponse } from '@@core/utils/dtos/openapi.respone.dto'; +import { ConnectionsError } from '@@core/utils/errors'; +import { AuthStrategy, CONNECTORS_METADATA } from '@panora/shared'; import { Connection } from './@utils/types'; export type StateDataType = { @@ -84,30 +84,7 @@ export class ConnectionsController { }); } - let stateData: StateDataType; - - // Step 1: Check for HTML entities - if (state.includes('"') || state.includes('&')) { - // Step 2: Replace HTML entities - const decodedState = state - .replace(/"/g, '"') // Replace " with " - .replace(/&/g, '&'); // Replace & with & - - // Step 3: Parse the JSON - stateData = JSON.parse(decodedState); - } else if (state.includes('squarespace_delimiter')) { - // squarespace asks for a random alphanumeric value - // Split the random part and the base64 part - const [randomPart, base64Part] = decodeURIComponent(state).split( - 'squarespace_delimiter', - ); - // Decode the base64 part to get the original JSON - const jsonString = Buffer.from(base64Part, 'base64').toString('utf-8'); - stateData = JSON.parse(jsonString); - } else { - // If no HTML entities are present, parse directly - stateData = JSON.parse(state); - } + const stateData: StateDataType = this.parseStateData(state); const { projectId, @@ -133,48 +110,18 @@ export class ConnectionsController { }, 'oauth2', ); - if (providerName == 'shopify') { - // we must redirect using shop and host to get a valid session on shopify server + + if (providerName === 'shopify') { service.redirectUponConnection(res, ...otherQueryParams); } else { res.redirect(returnUrl); } - if ( - CONNECTORS_METADATA[vertical.toLowerCase()][providerName.toLowerCase()] - .active !== false - ) { - this.logger.log('triggering initial core sync for all objects...;'); - // Performing Core Sync Service for active connectors - await this.coreSync.initialSync( - vertical.toLowerCase(), - providerName, - linkedUserId, - ); - } - } catch (error) { - throw error; - } - } - /*@Get('/gorgias/oauth/install') - handleGorgiasAuthUrl( - @Res() res: Response, - @Query('account') account: string, - @Query('response_type') response_type: string, - @Query('nonce') nonce: string, - @Query('scope') scope: string, - @Query('client_id') client_id: string, - @Query('redirect_uri') redirect_uri: string, - @Query('state') state: string, - ) { - try { - if (!account) throw new ReferenceError('account prop not found'); - const params = `?client_id=${client_id}&response_type=${response_type}&redirect_uri=${redirect_uri}&state=${state}&nonce=${nonce}&scope=${scope}`; - res.redirect(`https://${account}.gorgias.com/oauth/authorize${params}`); + await this.triggerInitialCoreSync(vertical, providerName, linkedUserId); } catch (error) { throw error; } - }*/ + } @ApiOperation({ operationId: 'handleApiKeyCallback', @@ -198,49 +145,32 @@ export class ConnectionsController { message: `No Callback Params found for state, found ${state}`, }); } + const stateData: StateDataType = JSON.parse(decodeURIComponent(state)); const { projectId, vertical, linkedUserId, providerName } = stateData; + const strategy = CONNECTORS_METADATA[vertical.toLowerCase()][providerName.toLowerCase()] .authStrategy.strategy; - const strategy_type = - strategy == AuthStrategy.api_key ? 'apikey' : 'basic'; + strategy === AuthStrategy.api_key ? 'apikey' : 'basic'; const service = this.categoryConnectionRegistry.getService( vertical.toLowerCase(), ); await service.handleCallBack( providerName, - { - projectId, - linkedUserId, - body, - }, + { projectId, linkedUserId, body }, strategy_type, ); - /*if ( - CONNECTORS_METADATA[vertical.toLowerCase()][providerName.toLowerCase()] - .active !== false - ) { - this.logger.log('triggering initial core sync for all objects...;'); - // Performing Core Sync Service for active connectors - await this.coreSync.initialSync( - vertical.toLowerCase(), - providerName, - linkedUserId, - ); - }*/ - res.redirect(`/`); + + res.redirect('/'); } catch (error) { throw error; } } - @ApiOperation({ - operationId: 'getConnections', - summary: 'List Connections', - }) + @ApiOperation({ operationId: 'getConnections', summary: 'List Connections' }) @ApiExcludeEndpoint() @ApiResponse({ status: 200 }) @UseGuards(JwtAuthGuard) @@ -249,19 +179,14 @@ export class ConnectionsController { try { const { id_project } = req.user; return await this.prisma.connections.findMany({ - where: { - id_project: id_project, - }, + where: { id_project }, }); } catch (error) { throw error; } } - @ApiOperation({ - operationId: 'getConnections', - summary: 'List Connections', - }) + @ApiOperation({ operationId: 'getConnections', summary: 'List Connections' }) @ApiGetArrayCustomResponse(Connection) @UseGuards(ApiKeyAuthGuard) @Get() @@ -269,12 +194,46 @@ export class ConnectionsController { try { const { id_project } = req.user; return await this.prisma.connections.findMany({ - where: { - id_project: id_project, - }, + where: { id_project }, }); } catch (error) { throw error; } } + + private parseStateData(state: string): StateDataType { + if (state.includes('"') || state.includes('&')) { + const decodedState = state.replace(/"/g, '"').replace(/&/g, '&'); + return JSON.parse(decodedState); + } else if ( + state.includes('deel_delimiter') || + state.includes('squarespace_delimiter') + ) { + const [, base64Part] = decodeURIComponent(state).split( + /deel_delimiter|squarespace_delimiter/, + ); + const jsonString = Buffer.from(base64Part, 'base64').toString('utf-8'); + return JSON.parse(jsonString); + } else { + return JSON.parse(state); + } + } + + private async triggerInitialCoreSync( + vertical: string, + providerName: string, + linkedUserId: string, + ) { + const isActive = + CONNECTORS_METADATA[vertical.toLowerCase()][providerName.toLowerCase()] + .active !== false; + if (isActive) { + this.logger.log('Triggering initial core sync for all objects...'); + await this.coreSync.initialSync( + vertical.toLowerCase(), + providerName, + linkedUserId, + ); + } + } } diff --git a/packages/api/src/@core/connections/connections.module.ts b/packages/api/src/@core/connections/connections.module.ts index 834317431..80c2a0d71 100644 --- a/packages/api/src/@core/connections/connections.module.ts +++ b/packages/api/src/@core/connections/connections.module.ts @@ -8,7 +8,7 @@ import { ConnectionsController } from './connections.controller'; import { CrmConnectionModule } from './crm/crm.connection.module'; import { FilestorageConnectionModule } from './filestorage/filestorage.connection.module'; import { HrisConnectionModule } from './hris/hris.connection.module'; -import { ManagementConnectionsModule } from './management/management.connection.module'; +import { ProductivityConnectionsModule } from './productivity/productivity.connection.module'; import { MarketingAutomationConnectionsModule } from './marketingautomation/marketingautomation.connection.module'; import { TicketingConnectionModule } from './ticketing/ticketing.connection.module'; import { EcommerceConnectionModule } from './ecommerce/ecommerce.connection.module'; @@ -17,7 +17,7 @@ import { EcommerceConnectionModule } from './ecommerce/ecommerce.connection.modu controllers: [ConnectionsController], imports: [ CrmConnectionModule, - ManagementConnectionsModule, + ProductivityConnectionsModule, TicketingConnectionModule, AccountingConnectionModule, AtsConnectionModule, @@ -39,7 +39,7 @@ import { EcommerceConnectionModule } from './ecommerce/ecommerce.connection.modu FilestorageConnectionModule, EcommerceConnectionModule, HrisConnectionModule, - ManagementConnectionsModule, + ProductivityConnectionsModule, ], }) export class ConnectionsModule {} diff --git a/packages/api/src/@core/connections/hris/services/deel/deel.service.ts b/packages/api/src/@core/connections/hris/services/deel/deel.service.ts index 531cf6332..c80edf18c 100644 --- a/packages/api/src/@core/connections/hris/services/deel/deel.service.ts +++ b/packages/api/src/@core/connections/hris/services/deel/deel.service.ts @@ -102,7 +102,9 @@ export class DeelConnectionService extends AbstractBaseConnectionService { //reconstruct the redirect URI that was passed in the githubend it must be the same const REDIRECT_URI = `${ - this.env.getPanoraBaseUrl() + this.env.getDistributionMode() == 'selfhost' + ? this.env.getTunnelIngress() + : this.env.getPanoraBaseUrl() }/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( @@ -190,7 +192,9 @@ export class DeelConnectionService extends AbstractBaseConnectionService { try { const { connectionId, refreshToken, projectId } = opts; const REDIRECT_URI = `${ - this.env.getPanoraBaseUrl() + this.env.getDistributionMode() == 'selfhost' + ? this.env.getTunnelIngress() + : this.env.getPanoraBaseUrl() }/connections/oauth/callback`; const formData = new URLSearchParams({ diff --git a/packages/api/src/@core/connections/hris/services/gusto/gusto.service.ts b/packages/api/src/@core/connections/hris/services/gusto/gusto.service.ts index 720052d8f..101a85a67 100644 --- a/packages/api/src/@core/connections/hris/services/gusto/gusto.service.ts +++ b/packages/api/src/@core/connections/hris/services/gusto/gusto.service.ts @@ -65,9 +65,9 @@ export class GustoConnectionService extends AbstractBaseConnectionService { }, }); - config.headers['Authorization'] = `Basic ${Buffer.from( - `${this.cryptoService.decrypt(connection.access_token)}:`, - ).toString('base64')}`; + config.headers['Authorization'] = `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`; config.headers = { ...config.headers, @@ -185,9 +185,7 @@ export class GustoConnectionService extends AbstractBaseConnectionService { async handleTokenRefresh(opts: RefreshParams) { try { const { connectionId, refreshToken, projectId } = opts; - const REDIRECT_URI = `${ - this.env.getPanoraBaseUrl() - }/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, diff --git a/packages/api/src/@core/connections/management/management.connection.module.ts b/packages/api/src/@core/connections/productivity/productivity.connection.module.ts similarity index 81% rename from packages/api/src/@core/connections/management/management.connection.module.ts rename to packages/api/src/@core/connections/productivity/productivity.connection.module.ts index cba149655..686c595b5 100644 --- a/packages/api/src/@core/connections/management/management.connection.module.ts +++ b/packages/api/src/@core/connections/productivity/productivity.connection.module.ts @@ -4,14 +4,14 @@ import { WebhookModule } from '@@core/@core-services/webhooks/panora-webhooks/we import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; import { Module } from '@nestjs/common'; -import { ManagementConnectionsService } from './services/management.connection.service'; +import { ProductivityConnectionsService } from './services/productivity.connection.service'; import { NotionConnectionService } from './services/notion/notion.service'; import { ServiceRegistry } from './services/registry.service'; import { SlackConnectionService } from './services/slack/slack.service'; @Module({ imports: [WebhookModule, BullQueueModule], providers: [ - ManagementConnectionsService, + ProductivityConnectionsService, WebhookService, EnvironmentService, ServiceRegistry, @@ -20,6 +20,6 @@ import { SlackConnectionService } from './services/slack/slack.service'; NotionConnectionService, SlackConnectionService, ], - exports: [ManagementConnectionsService], + exports: [ProductivityConnectionsService], }) -export class ManagementConnectionsModule {} +export class ProductivityConnectionsModule {} diff --git a/packages/api/src/@core/connections/management/services/notion/notion.service.ts b/packages/api/src/@core/connections/productivity/services/notion/notion.service.ts similarity index 92% rename from packages/api/src/@core/connections/management/services/notion/notion.service.ts rename to packages/api/src/@core/connections/productivity/services/notion/notion.service.ts index 89a7c91a2..83efd4444 100644 --- a/packages/api/src/@core/connections/management/services/notion/notion.service.ts +++ b/packages/api/src/@core/connections/productivity/services/notion/notion.service.ts @@ -48,7 +48,7 @@ export class NotionConnectionService extends AbstractBaseConnectionService { super(prisma, cryptoService); this.logger.setContext(NotionConnectionService.name); this.registry.registerService('notion', this); - this.type = providerToType('notion', 'management', AuthStrategy.oauth2); + this.type = providerToType('notion', 'productivity', AuthStrategy.oauth2); } async passthrough( @@ -81,7 +81,7 @@ export class NotionConnectionService extends AbstractBaseConnectionService { data: config.data, headers: config.headers, }, - 'management.notion.passthrough', + 'productivity.notion.passthrough', config.linkedUserId, ); } catch (error) { @@ -100,7 +100,7 @@ export class NotionConnectionService extends AbstractBaseConnectionService { where: { id_linked_user: linkedUserId, provider_slug: 'notion', - vertical: 'management', + vertical: 'productivity', }, }); @@ -130,7 +130,7 @@ export class NotionConnectionService extends AbstractBaseConnectionService { ); const data: NotionOAuthResponse = res.data; this.logger.log( - 'OAuth credentials : notion management ' + JSON.stringify(data), + 'OAuth credentials : notion productivity ' + JSON.stringify(data), ); let db_res; @@ -143,7 +143,7 @@ export class NotionConnectionService extends AbstractBaseConnectionService { }, data: { access_token: this.cryptoService.encrypt(data.access_token), - account_url: CONNECTORS_METADATA['management']['notion'].urls + account_url: CONNECTORS_METADATA['productivity']['notion'].urls .apiUrl as string, status: 'valid', created_at: new Date(), @@ -155,9 +155,9 @@ export class NotionConnectionService extends AbstractBaseConnectionService { id_connection: uuidv4(), connection_token: connection_token, provider_slug: 'notion', - vertical: 'management', + vertical: 'productivity', token_type: 'oauth2', - account_url: CONNECTORS_METADATA['management']['notion'].urls + account_url: CONNECTORS_METADATA['productivity']['notion'].urls .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), status: 'valid', diff --git a/packages/api/src/@core/connections/management/services/management.connection.service.ts b/packages/api/src/@core/connections/productivity/services/productivity.connection.service.ts similarity index 95% rename from packages/api/src/@core/connections/management/services/management.connection.service.ts rename to packages/api/src/@core/connections/productivity/services/productivity.connection.service.ts index 6cc6ac021..2182004b5 100644 --- a/packages/api/src/@core/connections/management/services/management.connection.service.ts +++ b/packages/api/src/@core/connections/productivity/services/productivity.connection.service.ts @@ -15,7 +15,7 @@ import { CategoryConnectionRegistry } from '@@core/@core-services/registries/con import { PassthroughResponse } from '@@core/passthrough/types'; @Injectable() -export class ManagementConnectionsService implements IConnectionCategory { +export class ProductivityConnectionsService implements IConnectionCategory { constructor( private serviceRegistry: ServiceRegistry, private connectionCategoryRegistry: CategoryConnectionRegistry, @@ -23,8 +23,8 @@ export class ManagementConnectionsService implements IConnectionCategory { private logger: LoggerService, private prisma: PrismaService, ) { - this.logger.setContext(ManagementConnectionsService.name); - this.connectionCategoryRegistry.registerService('management', this); + this.logger.setContext(ProductivityConnectionsService.name); + this.connectionCategoryRegistry.registerService('productivity', this); } //STEP 1:[FRONTEND STEP] //create a frontend SDK snippet in which an authorization embedded link is set up so when users click diff --git a/packages/api/src/@core/connections/management/services/registry.service.ts b/packages/api/src/@core/connections/productivity/services/registry.service.ts similarity index 100% rename from packages/api/src/@core/connections/management/services/registry.service.ts rename to packages/api/src/@core/connections/productivity/services/registry.service.ts diff --git a/packages/api/src/@core/connections/management/services/slack/slack.service.ts b/packages/api/src/@core/connections/productivity/services/slack/slack.service.ts similarity index 92% rename from packages/api/src/@core/connections/management/services/slack/slack.service.ts rename to packages/api/src/@core/connections/productivity/services/slack/slack.service.ts index 782dd538a..03a7b867b 100644 --- a/packages/api/src/@core/connections/management/services/slack/slack.service.ts +++ b/packages/api/src/@core/connections/productivity/services/slack/slack.service.ts @@ -64,7 +64,7 @@ export class SlackConnectionService extends AbstractBaseConnectionService { super(prisma, cryptoService); this.logger.setContext(SlackConnectionService.name); this.registry.registerService('slack', this); - this.type = providerToType('slack', 'management', AuthStrategy.oauth2); + this.type = providerToType('slack', 'productivity', AuthStrategy.oauth2); } async passthrough( @@ -97,7 +97,7 @@ export class SlackConnectionService extends AbstractBaseConnectionService { data: config.data, headers: config.headers, }, - 'management.slack.passthrough', + 'productivity.slack.passthrough', config.linkedUserId, ); } catch (error) { @@ -116,7 +116,7 @@ export class SlackConnectionService extends AbstractBaseConnectionService { where: { id_linked_user: linkedUserId, provider_slug: 'slack', - vertical: 'management', + vertical: 'productivity', }, }); @@ -142,7 +142,7 @@ export class SlackConnectionService extends AbstractBaseConnectionService { ); const data: SlackOAuthResponse = res.data; this.logger.log( - 'OAuth credentials : slack management ' + JSON.stringify(data), + 'OAuth credentials : slack productivity ' + JSON.stringify(data), ); let db_res; @@ -157,7 +157,7 @@ export class SlackConnectionService extends AbstractBaseConnectionService { access_token: this.cryptoService.encrypt( data.authed_user.access_token, ), - account_url: CONNECTORS_METADATA['management']['slack'].urls + account_url: CONNECTORS_METADATA['productivity']['slack'].urls .apiUrl as string, status: 'valid', created_at: new Date(), @@ -169,9 +169,9 @@ export class SlackConnectionService extends AbstractBaseConnectionService { id_connection: uuidv4(), connection_token: connection_token, provider_slug: 'slack', - vertical: 'management', + vertical: 'productivity', token_type: 'oauth2', - account_url: CONNECTORS_METADATA['management']['slack'].urls + account_url: CONNECTORS_METADATA['productivity']['slack'].urls .apiUrl as string, access_token: this.cryptoService.encrypt( data.authed_user.access_token, diff --git a/packages/api/src/@core/passthrough/passthrough.controller.ts b/packages/api/src/@core/passthrough/passthrough.controller.ts index ee9ed659b..9772a8a00 100644 --- a/packages/api/src/@core/passthrough/passthrough.controller.ts +++ b/packages/api/src/@core/passthrough/passthrough.controller.ts @@ -52,6 +52,7 @@ export class PassthroughController { remoteSource: integrationId, connectionId, vertical, + projectId, } = await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); @@ -61,6 +62,7 @@ export class PassthroughController { linkedUserId, vertical, connectionId, + projectId, ); } diff --git a/packages/api/src/@core/passthrough/passthrough.service.ts b/packages/api/src/@core/passthrough/passthrough.service.ts index 465d9d5bb..d43fdc7d4 100644 --- a/packages/api/src/@core/passthrough/passthrough.service.ts +++ b/packages/api/src/@core/passthrough/passthrough.service.ts @@ -23,15 +23,22 @@ export class PassthroughService { linkedUserId: string, vertical: string, connectionId: string, + projectId: string, ): Promise { try { - const { method, path, data, request_format, overrideBaseUrl, headers } = - requestParams; + const { + method, + path, + data, + request_format = 'JSON', + overrideBaseUrl, + headers, + } = requestParams; const job_resp_create = await this.prisma.events.create({ data: { id_connection: connectionId, - id_project: '', + id_project: projectId, id_event: uuidv4(), status: 'initialized', // Use whatever status is appropriate type: 'pull', @@ -68,7 +75,7 @@ export class PassthroughService { id_event: job_resp_create.id_event, }, data: { - status: status || (response as AxiosResponse).status, + status: String(status) || String((response as AxiosResponse).status), }, }); diff --git a/packages/api/src/@core/passthrough/types/index.ts b/packages/api/src/@core/passthrough/types/index.ts index f74c20b63..3ad9e9a4a 100644 --- a/packages/api/src/@core/passthrough/types/index.ts +++ b/packages/api/src/@core/passthrough/types/index.ts @@ -1,5 +1,10 @@ -import { AxiosResponse } from 'axios'; +type BaseResponse = { + status: number; + statusText: string; + headers: any; + data: any; +}; export type PassthroughResponse = - | AxiosResponse + | BaseResponse | { statusCode: number; retryId: string }; diff --git a/packages/api/src/@core/sync/sync.service.ts b/packages/api/src/@core/sync/sync.service.ts index 36f78de5a..f3b870096 100644 --- a/packages/api/src/@core/sync/sync.service.ts +++ b/packages/api/src/@core/sync/sync.service.ts @@ -30,6 +30,12 @@ export class CoreSyncService { case ConnectorCategory.Ats: await this.handleAtsSync(provider, linkedUserId); break; + case ConnectorCategory.Hris: + await this.handleHrisSync(provider, linkedUserId); + break; + case ConnectorCategory.Accounting: + await this.handleAccountingSync(provider, linkedUserId); + break; case ConnectorCategory.Ecommerce: await this.handleEcommerceSync(provider, linkedUserId); break; @@ -39,6 +45,149 @@ export class CoreSyncService { } } + // todo + async handleAccountingSync(provider: string, linkedUserId: string) { + return; + } + + async handleHrisSync(provider: string, linkedUserId: string) { + // add other objects when i have info on the order + //todo: define here the topological order PER provider + const tasks = [ + () => + this.registry.getService('hris', 'company').syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUserId, + }), + ]; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: provider.toLowerCase(), + }, + }); + + for (const task of tasks) { + try { + await task(); + } catch (error) { + this.logger.error(`Task failed: ${error.message}`, error); + } + } + const companies = await this.prisma.hris_companies.findMany({ + where: { + id_connection: connection.id_connection, + }, + }); + + const companiesEmployeeTasks = companies.map( + (company) => async () => + this.registry.getService('hris', 'employee').syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUserId, + id_company: company.id_hris_company, + }), + ); + + const companiesEmployerBenefitsTasks = companies.map( + (company) => async () => + this.registry.getService('hris', 'employerbenefit').syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUserId, + id_company: company.id_hris_company, + }), + ); + + const companiesGroupsTasks = companies.map( + (company) => async () => + this.registry.getService('hris', 'group').syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUserId, + id_company: company.id_hris_company, + }), + ); + + const employees = await this.prisma.hris_employees.findMany({ + where: { + id_connection: connection.id_connection, + }, + }); + + const employeesBenefitsTasks = employees.map( + (employee) => async () => + this.registry.getService('hris', 'benefit').syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUserId, + id_employee: employee.id_hris_employee, + }), + ); + + const employeesLocationsTasks = employees.map( + (employee) => async () => + this.registry.getService('hris', 'location').syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUserId, + id_employee: employee.id_hris_employee, + }), + ); + + for (const task of companiesEmployeeTasks) { + try { + await task(); + } catch (error) { + this.logger.error( + `Companies Employee task failed: ${error.message}`, + error, + ); + } + } + + for (const task of employeesLocationsTasks) { + try { + await task(); + } catch (error) { + this.logger.error( + `Companies Location task failed: ${error.message}`, + error, + ); + } + } + + for (const task of companiesEmployerBenefitsTasks) { + try { + await task(); + } catch (error) { + this.logger.error( + `Companies Employer Benefits task failed: ${error.message}`, + error, + ); + } + } + + for (const task of companiesGroupsTasks) { + try { + await task(); + } catch (error) { + this.logger.error( + `Companies Groups task failed: ${error.message}`, + error, + ); + } + } + + for (const task of employeesBenefitsTasks) { + try { + await task(); + } catch (error) { + this.logger.error( + `Employees Benefits task failed: ${error.message}`, + error, + ); + } + } + } + async handleCrmSync(provider: string, linkedUserId: string) { const tasks = [ () => diff --git a/packages/api/src/@core/utils/decorators/utils.ts b/packages/api/src/@core/utils/decorators/utils.ts new file mode 100644 index 000000000..3f0fc49e3 --- /dev/null +++ b/packages/api/src/@core/utils/decorators/utils.ts @@ -0,0 +1,98 @@ +import { + CRM_PROVIDERS, + HRIS_PROVIDERS, + ATS_PROVIDERS, + ACCOUNTING_PROVIDERS, + TICKETING_PROVIDERS, + MARKETINGAUTOMATION_PROVIDERS, + FILESTORAGE_PROVIDERS, + ECOMMERCE_PROVIDERS, + EcommerceObject, + CrmObject, + FileStorageObject, + TicketingObject, + HrisObject, + AccountingObject, + MarketingAutomationObject, + AtsObject, +} from '@panora/shared'; +import * as fs from 'fs'; +import * as path from 'path'; + +interface ProviderMetadata { + actions: string[]; + supportedFields: string[][]; +} + +export async function generatePanoraParamsSpec(spec: any) { + const verticals = { + crm: [CRM_PROVIDERS, CrmObject], + hris: [HRIS_PROVIDERS, HrisObject], + ats: [ATS_PROVIDERS, AtsObject], + accounting: [ACCOUNTING_PROVIDERS, AccountingObject], + ticketing: [TICKETING_PROVIDERS, TicketingObject], + marketingautomation: [ + MARKETINGAUTOMATION_PROVIDERS, + MarketingAutomationObject, + ], + filestorage: [FILESTORAGE_PROVIDERS, FileStorageObject], + ecommerce: [ECOMMERCE_PROVIDERS, EcommerceObject], + }; + + for (const [vertical, [providers, COMMON_OBJECTS]] of Object.entries( + verticals, + )) { + for (const objectKey of Object.values(COMMON_OBJECTS)) { + for (const provider of providers as string[]) { + try { + const metadataPath = path.join( + process.cwd(), + 'src', + vertical.toLowerCase(), + objectKey as string, + 'services', + provider, + 'metadata.json', + ); + + const metadataRaw = fs.readFileSync(metadataPath, 'utf8'); + const metadata: ProviderMetadata = JSON.parse(metadataRaw); + + if (metadata) { + metadata.actions.forEach((action, index) => { + const path = `/${vertical.toLowerCase()}/${objectKey}s`; + const op = + action === 'list' ? 'get' : action === 'create' ? 'post' : ''; + + if (spec.paths[path] && spec.paths[path][op]) { + if (!spec.paths[path][op]['x-panora-remote-platforms']) { + spec.paths[path][op]['x-panora-remote-platforms'] = {}; + } + // Ensure the provider array is initialized + if ( + !spec.paths[path][op]['x-panora-remote-platforms'][provider] + ) { + spec.paths[path][op]['x-panora-remote-platforms'][provider] = + []; // Initialize as an array + } + for (const field of metadata.supportedFields[index]) { + spec.paths[path][op]['x-panora-remote-platforms'][ + provider + ].push(field); + } + } else { + console.warn( + `Path or operation not found in spec: ${path} ${op}`, + ); + } + }); + } + } catch (error) { + console.error(error); + } + } + } + } + + return spec; +} diff --git a/packages/api/src/@core/utils/types/original/original.hris.ts b/packages/api/src/@core/utils/types/original/original.hris.ts index ccb190529..e85d40144 100644 --- a/packages/api/src/@core/utils/types/original/original.hris.ts +++ b/packages/api/src/@core/utils/types/original/original.hris.ts @@ -1,5 +1,13 @@ /* INPUT */ +import { GustoBenefitOutput } from '@hris/benefit/services/gusto/types'; +import { GustoCompanyOutput } from '@hris/company/services/gusto/types'; +import { GustoEmployeeOutput } from '@hris/employee/services/gusto/types'; +import { GustoEmployerbenefitOutput } from '@hris/employerbenefit/services/gusto/types'; +import { GustoEmploymentOutput } from '@hris/employment/services/gusto/types'; +import { GustoGroupOutput } from '@hris/group/services/gusto/types'; +import { GustoLocationOutput } from '@hris/location/services/gusto/types'; + /* bankinfo */ export type OriginalBankInfoInput = any; @@ -42,6 +50,9 @@ export type OriginalTimeoffInput = any; /* timeoffbalance */ export type OriginalTimeoffBalanceInput = any; +/* timesheetentry */ +export type OriginalTimesheetentryInput = any; + export type HrisObjectInput = | OriginalBankInfoInput | OriginalBenefitInput @@ -56,7 +67,8 @@ export type HrisObjectInput = | OriginalPayGroupInput | OriginalPayrollRunInput | OriginalTimeoffInput - | OriginalTimeoffBalanceInput; + | OriginalTimeoffBalanceInput + | OriginalTimesheetentryInput; /* OUTPUT */ @@ -64,31 +76,31 @@ export type HrisObjectInput = export type OriginalBankInfoOutput = any; /* benefit */ -export type OriginalBenefitOutput = any; +export type OriginalBenefitOutput = GustoBenefitOutput; /* company */ -export type OriginalCompanyOutput = any; +export type OriginalCompanyOutput = GustoCompanyOutput; /* dependent */ export type OriginalDependentOutput = any; /* employee */ -export type OriginalEmployeeOutput = any; +export type OriginalEmployeeOutput = GustoEmployeeOutput; /* employeepayrollrun */ export type OriginalEmployeePayrollRunOutput = any; /* employerbenefit */ -export type OriginalEmployerBenefitOutput = any; +export type OriginalEmployerBenefitOutput = GustoEmployerbenefitOutput; /* employment */ -export type OriginalEmploymentOutput = any; +export type OriginalEmploymentOutput = GustoEmploymentOutput; /* group */ -export type OriginalGroupOutput = any; +export type OriginalGroupOutput = GustoGroupOutput; /* location */ -export type OriginalLocationOutput = any; +export type OriginalLocationOutput = GustoLocationOutput; /* paygroup */ export type OriginalPayGroupOutput = any; @@ -102,6 +114,9 @@ export type OriginalTimeoffOutput = any; /* timeoffbalance */ export type OriginalTimeoffBalanceOutput = any; +/* timesheetentry */ +export type OriginalTimesheetentryOutput = any; + export type HrisObjectOutput = | OriginalBankInfoOutput | OriginalBenefitOutput @@ -116,4 +131,5 @@ export type HrisObjectOutput = | OriginalPayGroupOutput | OriginalPayrollRunOutput | OriginalTimeoffOutput - | OriginalTimeoffBalanceOutput; + | OriginalTimeoffBalanceOutput + | OriginalTimesheetentryOutput; diff --git a/packages/api/src/accounting/account/account.controller.ts b/packages/api/src/accounting/account/account.controller.ts index c4f8b1a71..9625017e6 100644 --- a/packages/api/src/accounting/account/account.controller.ts +++ b/packages/api/src/accounting/account/account.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -57,6 +59,7 @@ export class AccountController { example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) @ApiPaginatedResponse(UnifiedAccountingAccountOutput) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @UseGuards(ApiKeyAuthGuard) @Get() async getAccounts( diff --git a/packages/api/src/accounting/account/services/account.service.ts b/packages/api/src/accounting/account/services/account.service.ts index 526617587..4b1e0545b 100644 --- a/packages/api/src/accounting/account/services/account.service.ts +++ b/packages/api/src/accounting/account/services/account.service.ts @@ -1,12 +1,14 @@ -import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { v4 as uuidv4 } from 'uuid'; +import { ApiResponse, CurrencyCode } from '@@core/utils/types'; +import { throwTypedError } from '@@core/utils/errors'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { Injectable } from '@nestjs/common'; import { UnifiedAccountingAccountInput, UnifiedAccountingAccountOutput, } from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; @@ -28,18 +30,120 @@ export class AccountService { linkedUserId: string, remote_data?: boolean, ): Promise { - return; + try { + const service = this.serviceRegistry.getService(integrationId); + const resp = await service.addAccount(unifiedAccountData, linkedUserId); + + const savedAccount = await this.prisma.acc_accounts.create({ + data: { + id_acc_account: uuidv4(), + ...unifiedAccountData, + current_balance: unifiedAccountData.current_balance + ? Number(unifiedAccountData.current_balance) + : null, + remote_id: resp.data.remote_id, + id_connection: resp.data.id_connection, + created_at: new Date(), + modified_at: new Date(), + }, + }); + + const result: UnifiedAccountingAccountOutput = { + ...savedAccount, + currency: savedAccount.currency as CurrencyCode, + id: savedAccount.id_acc_account, + current_balance: savedAccount.current_balance + ? Number(savedAccount.current_balance) + : undefined, + }; + + if (remote_data) { + result.remote_data = resp.data; + } + + return result; + } catch (error) { + throw error; + } } async getAccount( - id_accounting_account: string, + id_acc_account: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const account = await this.prisma.acc_accounts.findUnique({ + where: { id_acc_account: id_acc_account }, + }); + + if (!account) { + throw new Error(`Account with ID ${id_acc_account} not found.`); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: account.id_acc_account }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedAccount: UnifiedAccountingAccountOutput = { + id: account.id_acc_account, + name: account.name, + description: account.description, + classification: account.classification, + type: account.type, + status: account.status, + current_balance: account.current_balance + ? Number(account.current_balance) + : undefined, + currency: account.currency as CurrencyCode, + account_number: account.account_number, + parent_account: account.parent_account, + company_info_id: account.id_acc_company_info, + field_mappings: field_mappings, + remote_id: account.remote_id, + created_at: account.created_at, + modified_at: account.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: account.id_acc_account }, + }); + unifiedAccount.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.account.pull', + method: 'GET', + url: '/accounting/account', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedAccount; + } catch (error) { + throw error; + } } async getAccounts( @@ -50,7 +154,93 @@ export class AccountService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingAccountOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const accounts = await this.prisma.acc_accounts.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_account: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = accounts.length > limit; + if (hasNextPage) accounts.pop(); + + const unifiedAccounts = await Promise.all( + accounts.map(async (account) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: account.id_acc_account }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedAccount: UnifiedAccountingAccountOutput = { + id: account.id_acc_account, + name: account.name, + description: account.description, + classification: account.classification, + type: account.type, + status: account.status, + current_balance: account.current_balance + ? Number(account.current_balance) + : undefined, + currency: account.currency as CurrencyCode, + account_number: account.account_number, + parent_account: account.parent_account, + company_info_id: account.id_acc_company_info, + field_mappings: field_mappings, + remote_id: account.remote_id, + created_at: account.created_at, + modified_at: account.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: account.id_acc_account }, + }); + unifiedAccount.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedAccount; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.account.pull', + method: 'GET', + url: '/accounting/accounts', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedAccounts, + next_cursor: hasNextPage + ? accounts[accounts.length - 1].id_acc_account + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/account/sync/sync.service.ts b/packages/api/src/accounting/account/sync/sync.service.ts index 7e6d8604b..1f6584b6b 100644 --- a/packages/api/src/accounting/account/sync/sync.service.ts +++ b/packages/api/src/accounting/account/sync/sync.service.ts @@ -1,10 +1,21 @@ +import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { Cron } from '@nestjs/schedule'; +import { ApiResponse, CurrencyCode } from '@@core/utils/types'; +import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; -import { Injectable, OnModuleInit } from '@nestjs/common'; import { ServiceRegistry } from '../services/registry.service'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { UnifiedAccountingAccountOutput } from '../types/model.unified'; +import { IAccountService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_accounts as AccAccount } from '@prisma/client'; +import { OriginalAccountOutput } from '@@core/utils/types/original/original.accounting'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -14,23 +25,142 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'account', this); + } + + async onModuleInit() { + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting accounts...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IAccountService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingAccountOutput, + OriginalAccountOutput, + IAccountService + >(integrationId, linkedUserId, 'accounting', 'account', service, []); + } catch (error) { + throw error; + } } - saveToDb( + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + accounts: UnifiedAccountingAccountOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const accountResults: AccAccount[] = []; - async onModuleInit() { - // Initialization logic - } + for (let i = 0; i < accounts.length; i++) { + const account = accounts[i]; + const originId = account.remote_id; + + let existingAccount = await this.prisma.acc_accounts.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); - // Additional methods and logic + const accountData = { + name: account.name, + description: account.description, + classification: account.classification, + type: account.type, + status: account.status, + current_balance: account.current_balance + ? Number(account.current_balance) + : null, + currency: account.currency as CurrencyCode, + account_number: account.account_number, + parent_account: account.parent_account, + id_acc_company_info: account.company_info_id, + remote_id: originId, + modified_at: new Date(), + }; + + if (existingAccount) { + existingAccount = await this.prisma.acc_accounts.update({ + where: { id_acc_account: existingAccount.id_acc_account }, + data: accountData, + }); + } else { + existingAccount = await this.prisma.acc_accounts.create({ + data: { + ...accountData, + id_acc_account: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + accountResults.push(existingAccount); + + // Process field mappings + await this.ingestService.processFieldMappings( + account.field_mappings, + existingAccount.id_acc_account, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingAccount.id_acc_account, + remote_data[i], + ); + } + + return accountResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/accounting/account/types/index.ts b/packages/api/src/accounting/account/types/index.ts index 6164e8f8d..d04929121 100644 --- a/packages/api/src/accounting/account/types/index.ts +++ b/packages/api/src/accounting/account/types/index.ts @@ -1,7 +1,11 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { UnifiedAccountingAccountInput, UnifiedAccountingAccountOutput } from './model.unified'; +import { + UnifiedAccountingAccountInput, + UnifiedAccountingAccountOutput, +} from './model.unified'; import { OriginalAccountOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IAccountService { addAccount( @@ -9,10 +13,7 @@ export interface IAccountService { linkedUserId: string, ): Promise>; - syncAccounts( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IAccountMapper { diff --git a/packages/api/src/accounting/account/types/model.unified.ts b/packages/api/src/accounting/account/types/model.unified.ts index 087f5cfa3..25a221d9d 100644 --- a/packages/api/src/accounting/account/types/model.unified.ts +++ b/packages/api/src/accounting/account/types/model.unified.ts @@ -1,3 +1,181 @@ -export class UnifiedAccountingAccountInput {} +import { CurrencyCode } from '@@core/utils/types'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsNumber, + IsDateString, +} from 'class-validator'; -export class UnifiedAccountingAccountOutput extends UnifiedAccountingAccountInput {} +export class UnifiedAccountingAccountInput { + @ApiPropertyOptional({ + type: String, + example: 'Cash', + nullable: true, + description: 'The name of the account', + }) + @IsString() + @IsOptional() + name?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Main cash account for daily operations', + nullable: true, + description: 'A description of the account', + }) + @IsString() + @IsOptional() + description?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Asset', + nullable: true, + description: 'The classification of the account', + }) + @IsString() + @IsOptional() + classification?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Current Asset', + nullable: true, + description: 'The type of the account', + }) + @IsString() + @IsOptional() + type?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Active', + nullable: true, + description: 'The status of the account', + }) + @IsString() + @IsOptional() + status?: string; + + @ApiPropertyOptional({ + type: Number, + example: 10000, + nullable: true, + description: 'The current balance of the account', + }) + @IsNumber() + @IsOptional() + current_balance?: number; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + enum: CurrencyCode, + nullable: true, + description: 'The currency of the account', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: String, + example: '1000', + nullable: true, + description: 'The account number', + }) + @IsString() + @IsOptional() + account_number?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the parent account', + }) + @IsUUID() + @IsOptional() + parent_account?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company info', + }) + @IsUUID() + @IsOptional() + company_info_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingAccountOutput extends UnifiedAccountingAccountInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the account record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'account_1234', + nullable: true, + description: 'The remote ID of the account in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the account in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the account record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the account record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} diff --git a/packages/api/src/accounting/accounting.module.ts b/packages/api/src/accounting/accounting.module.ts index c58d7cbaa..a6ea75d8f 100644 --- a/packages/api/src/accounting/accounting.module.ts +++ b/packages/api/src/accounting/accounting.module.ts @@ -19,6 +19,7 @@ import { TaxRateModule } from './taxrate/taxrate.module'; import { TrackingCategoryModule } from './trackingcategory/trackingcategory.module'; import { TransactionModule } from './transaction/transaction.module'; import { VendorCreditModule } from './vendorcredit/vendorcredit.module'; +import { AccountingUnificationService } from './@lib/@unification'; @Module({ exports: [ @@ -43,6 +44,7 @@ import { VendorCreditModule } from './vendorcredit/vendorcredit.module'; TransactionModule, VendorCreditModule, ], + providers: [AccountingUnificationService], imports: [ AccountModule, AddressModule, diff --git a/packages/api/src/accounting/address/address.controller.ts b/packages/api/src/accounting/address/address.controller.ts index e0f895f29..33a2431fb 100644 --- a/packages/api/src/accounting/address/address.controller.ts +++ b/packages/api/src/accounting/address/address.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -33,7 +35,6 @@ import { ApiPaginatedResponse, } from '@@core/utils/dtos/openapi.respone.dto'; - @ApiTags('accounting/addresses') @Controller('accounting/addresses') export class AddressController { @@ -110,6 +111,7 @@ export class AddressController { }) @ApiGetCustomResponse(UnifiedAccountingAddressOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get(':id') async retrieve( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/accounting/address/services/address.service.ts b/packages/api/src/accounting/address/services/address.service.ts index ec453395a..e4b2a15ef 100644 --- a/packages/api/src/accounting/address/services/address.service.ts +++ b/packages/api/src/accounting/address/services/address.service.ts @@ -9,12 +9,9 @@ import { UnifiedAccountingAddressInput, UnifiedAccountingAddressOutput, } from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; -import { IAddressService } from '../types'; - @Injectable() export class AddressService { constructor( @@ -28,14 +25,79 @@ export class AddressService { } async getAddress( - id_addressing_address: string, + id_acc_address: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const address = await this.prisma.acc_addresses.findUnique({ + where: { id_acc_address: id_acc_address }, + }); + + if (!address) { + throw new Error(`Address with ID ${id_acc_address} not found.`); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: address.id_acc_address }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedAddress: UnifiedAccountingAddressOutput = { + id: address.id_acc_address, + type: address.type, + street_1: address.street_1, + street_2: address.street_2, + city: address.city, + state: address.state, + country_subdivision: address.country_subdivision, + country: address.country, + zip: address.zip, + contact_id: address.id_acc_contact, + company_info_id: address.id_acc_company_info, + field_mappings: field_mappings, + created_at: address.created_at, + modified_at: address.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: address.id_acc_address }, + }); + unifiedAddress.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.address.pull', + method: 'GET', + url: '/accounting/address', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedAddress; + } catch (error) { + throw error; + } } async getAddresss( @@ -46,7 +108,90 @@ export class AddressService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingAddressOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const addresses = await this.prisma.acc_addresses.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_address: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = addresses.length > limit; + if (hasNextPage) addresses.pop(); + + const unifiedAddresses = await Promise.all( + addresses.map(async (address) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: address.id_acc_address }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedAddress: UnifiedAccountingAddressOutput = { + id: address.id_acc_address, + type: address.type, + street_1: address.street_1, + street_2: address.street_2, + city: address.city, + state: address.state, + country_subdivision: address.country_subdivision, + country: address.country, + zip: address.zip, + contact_id: address.id_acc_contact, + company_info_id: address.id_acc_company_info, + field_mappings: field_mappings, + created_at: address.created_at, + modified_at: address.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: address.id_acc_address }, + }); + unifiedAddress.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedAddress; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.address.pull', + method: 'GET', + url: '/accounting/addresses', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedAddresses, + next_cursor: hasNextPage + ? addresses[addresses.length - 1].id_acc_address + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/address/sync/sync.service.ts b/packages/api/src/accounting/address/sync/sync.service.ts index 9821525dc..85a590437 100644 --- a/packages/api/src/accounting/address/sync/sync.service.ts +++ b/packages/api/src/accounting/address/sync/sync.service.ts @@ -10,6 +10,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedAccountingAddressOutput } from '../types/model.unified'; import { IAddressService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_addresses as AccAddress } from '@prisma/client'; +import { OriginalAddressOutput } from '@@core/utils/types/original/original.accounting'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -19,26 +25,139 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'address', this); } - saveToDb( + + async onModuleInit() { + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting addresses...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IAddressService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingAddressOutput, + OriginalAddressOutput, + IAddressService + >(integrationId, linkedUserId, 'accounting', 'address', service, []); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + addresses: UnifiedAccountingAddressOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } - removeInDb?(connection_id: string, remote_id: string): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const addressResults: AccAddress[] = []; - async onModuleInit() { - // Initialization logic - } + for (let i = 0; i < addresses.length; i++) { + const address = addresses[i]; + const originId = address.remote_id; + + let existingAddress = await this.prisma.acc_addresses.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const addressData = { + type: address.type, + street_1: address.street_1, + street_2: address.street_2, + city: address.city, + state: address.state, + country_subdivision: address.country_subdivision, + country: address.country, + zip: address.zip, + id_acc_contact: address.contact_id, + id_acc_company_info: address.company_info_id, + modified_at: new Date(), + }; - // Additional methods and logic + if (existingAddress) { + existingAddress = await this.prisma.acc_addresses.update({ + where: { id_acc_address: existingAddress.id_acc_address }, + data: addressData, + }); + } else { + existingAddress = await this.prisma.acc_addresses.create({ + data: { + ...addressData, + id_acc_address: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + addressResults.push(existingAddress); + + // Process field mappings + await this.ingestService.processFieldMappings( + address.field_mappings, + existingAddress.id_acc_address, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingAddress.id_acc_address, + remote_data[i], + ); + } + + return addressResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/accounting/address/types/index.ts b/packages/api/src/accounting/address/types/index.ts index 0d7caf1b1..b50aa6297 100644 --- a/packages/api/src/accounting/address/types/index.ts +++ b/packages/api/src/accounting/address/types/index.ts @@ -1,7 +1,11 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { UnifiedAccountingAddressInput, UnifiedAccountingAddressOutput } from './model.unified'; +import { + UnifiedAccountingAddressInput, + UnifiedAccountingAddressOutput, +} from './model.unified'; import { OriginalAddressOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IAddressService { addAddress( @@ -9,10 +13,7 @@ export interface IAddressService { linkedUserId: string, ): Promise>; - syncAddresss( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IAddressMapper { diff --git a/packages/api/src/accounting/address/types/model.unified.ts b/packages/api/src/accounting/address/types/model.unified.ts index 4ab21d9c6..72b534872 100644 --- a/packages/api/src/accounting/address/types/model.unified.ts +++ b/packages/api/src/accounting/address/types/model.unified.ts @@ -1,3 +1,174 @@ -export class UnifiedAccountingAddressInput {} +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsUUID, IsOptional, IsString, IsDateString } from 'class-validator'; -export class UnifiedAccountingAddressOutput extends UnifiedAccountingAddressInput {} +export class UnifiedAccountingAddressInput { + @ApiPropertyOptional({ + type: String, + example: 'Billing', + nullable: true, + description: 'The type of the address', + }) + @IsString() + @IsOptional() + type?: string; + + @ApiPropertyOptional({ + type: String, + example: '123 Main St', + nullable: true, + description: 'The first line of the street address', + }) + @IsString() + @IsOptional() + street_1?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Apt 4B', + nullable: true, + description: 'The second line of the street address', + }) + @IsString() + @IsOptional() + street_2?: string; + + @ApiPropertyOptional({ + type: String, + example: 'New York', + nullable: true, + description: 'The city of the address', + }) + @IsString() + @IsOptional() + city?: string; + + @ApiPropertyOptional({ + type: String, + example: 'NY', + nullable: true, + description: 'The state of the address', + }) + @IsString() + @IsOptional() + state?: string; + + @ApiPropertyOptional({ + type: String, + example: 'New York', + nullable: true, + description: + 'The country subdivision (e.g., province or state) of the address', + }) + @IsString() + @IsOptional() + country_subdivision?: string; + + @ApiPropertyOptional({ + type: String, + example: 'USA', + nullable: true, + description: 'The country of the address', + }) + @IsString() + @IsOptional() + country?: string; + + @ApiPropertyOptional({ + type: String, + example: '10001', + nullable: true, + description: 'The zip or postal code of the address', + }) + @IsString() + @IsOptional() + zip?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated contact', + }) + @IsUUID() + @IsOptional() + contact_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company info', + }) + @IsUUID() + @IsOptional() + company_info_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingAddressOutput extends UnifiedAccountingAddressInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the address record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'address_1234', + nullable: true, + description: 'The remote ID of the address in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the address in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the address record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the address record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} diff --git a/packages/api/src/accounting/attachment/attachment.controller.ts b/packages/api/src/accounting/attachment/attachment.controller.ts index e27f74307..afd5646b2 100644 --- a/packages/api/src/accounting/attachment/attachment.controller.ts +++ b/packages/api/src/accounting/attachment/attachment.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -109,6 +111,7 @@ export class AttachmentController { example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) @ApiGetCustomResponse(UnifiedAccountingAttachmentOutput) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @UseGuards(ApiKeyAuthGuard) @Get(':id') async retrieve( diff --git a/packages/api/src/accounting/attachment/services/attachment.service.ts b/packages/api/src/accounting/attachment/services/attachment.service.ts index f38b1538e..a99d30c6a 100644 --- a/packages/api/src/accounting/attachment/services/attachment.service.ts +++ b/packages/api/src/accounting/attachment/services/attachment.service.ts @@ -9,12 +9,9 @@ import { UnifiedAccountingAttachmentInput, UnifiedAccountingAttachmentOutput, } from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; -import { IAttachmentService } from '../types'; - @Injectable() export class AttachmentService { constructor( @@ -35,18 +32,107 @@ export class AttachmentService { linkedUserId: string, remote_data?: boolean, ): Promise { - return; + try { + const service = this.serviceRegistry.getService(integrationId); + const resp = await service.addAttachment( + unifiedAttachmentData, + linkedUserId, + ); + + const savedAttachment = await this.prisma.acc_attachments.create({ + data: { + id_acc_attachment: uuidv4(), + ...unifiedAttachmentData, + remote_id: resp.data.remote_id, + id_connection: connection_id, + created_at: new Date(), + modified_at: new Date(), + }, + }); + + const result: UnifiedAccountingAttachmentOutput = { + ...savedAttachment, + id: savedAttachment.id_acc_attachment, + }; + + if (remote_data) { + result.remote_data = resp.data; + } + + return result; + } catch (error) { + throw error; + } } async getAttachment( - id_attachmenting_attachment: string, + id_acc_attachment: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const attachment = await this.prisma.acc_attachments.findUnique({ + where: { id_acc_attachment: id_acc_attachment }, + }); + + if (!attachment) { + throw new Error(`Attachment with ID ${id_acc_attachment} not found.`); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: attachment.id_acc_attachment }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedAttachment: UnifiedAccountingAttachmentOutput = { + id: attachment.id_acc_attachment, + file_name: attachment.file_name, + file_url: attachment.file_url, + account_id: attachment.id_acc_account, + field_mappings: field_mappings, + remote_id: attachment.remote_id, + created_at: attachment.created_at, + modified_at: attachment.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: attachment.id_acc_attachment }, + }); + unifiedAttachment.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.attachment.pull', + method: 'GET', + url: '/accounting/attachment', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedAttachment; + } catch (error) { + throw error; + } } async getAttachments( @@ -57,7 +143,84 @@ export class AttachmentService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingAttachmentOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const attachments = await this.prisma.acc_attachments.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_attachment: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = attachments.length > limit; + if (hasNextPage) attachments.pop(); + + const unifiedAttachments = await Promise.all( + attachments.map(async (attachment) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: attachment.id_acc_attachment }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedAttachment: UnifiedAccountingAttachmentOutput = { + id: attachment.id_acc_attachment, + file_name: attachment.file_name, + file_url: attachment.file_url, + account_id: attachment.id_acc_account, + field_mappings: field_mappings, + remote_id: attachment.remote_id, + created_at: attachment.created_at, + modified_at: attachment.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: attachment.id_acc_attachment }, + }); + unifiedAttachment.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedAttachment; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.attachment.pull', + method: 'GET', + url: '/accounting/attachments', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedAttachments, + next_cursor: hasNextPage + ? attachments[attachments.length - 1].id_acc_attachment + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/attachment/sync/sync.service.ts b/packages/api/src/accounting/attachment/sync/sync.service.ts index 950182fde..a19b71b99 100644 --- a/packages/api/src/accounting/attachment/sync/sync.service.ts +++ b/packages/api/src/accounting/attachment/sync/sync.service.ts @@ -10,6 +10,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedAccountingAttachmentOutput } from '../types/model.unified'; import { IAttachmentService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_attachments as AccAttachment } from '@prisma/client'; +import { OriginalAttachmentOutput } from '@@core/utils/types/original/original.accounting'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -19,26 +25,146 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'attachment', this); } - saveToDb( + + async onModuleInit() { + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting attachments...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IAttachmentService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingAttachmentOutput, + OriginalAttachmentOutput, + IAttachmentService + >(integrationId, linkedUserId, 'accounting', 'attachment', service, []); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + attachments: UnifiedAccountingAttachmentOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } - removeInDb?(connection_id: string, remote_id: string): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const attachmentResults: AccAttachment[] = []; - async onModuleInit() { - // Initialization logic + for (let i = 0; i < attachments.length; i++) { + const attachment = attachments[i]; + const originId = attachment.remote_id; + + let existingAttachment = await this.prisma.acc_attachments.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const attachmentData = { + file_name: attachment.file_name, + file_url: attachment.file_url, + id_acc_account: attachment.account_id, + remote_id: originId, + modified_at: new Date(), + }; + + if (existingAttachment) { + existingAttachment = await this.prisma.acc_attachments.update({ + where: { id_acc_attachment: existingAttachment.id_acc_attachment }, + data: attachmentData, + }); + } else { + existingAttachment = await this.prisma.acc_attachments.create({ + data: { + ...attachmentData, + id_acc_attachment: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + attachmentResults.push(existingAttachment); + + // Process field mappings + await this.ingestService.processFieldMappings( + attachment.field_mappings, + existingAttachment.id_acc_attachment, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingAttachment.id_acc_attachment, + remote_data[i], + ); + } + + return attachmentResults; + } catch (error) { + throw error; + } } - // Additional methods and logic + async removeInDb(connection_id: string, remote_id: string): Promise { + try { + await this.prisma.acc_attachments.deleteMany({ + where: { + remote_id: remote_id, + id_connection: connection_id, + }, + }); + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/accounting/attachment/types/index.ts b/packages/api/src/accounting/attachment/types/index.ts index cd1e3c776..19864b2b9 100644 --- a/packages/api/src/accounting/attachment/types/index.ts +++ b/packages/api/src/accounting/attachment/types/index.ts @@ -5,6 +5,7 @@ import { } from './model.unified'; import { OriginalAttachmentOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IAttachmentService { addAttachment( @@ -12,10 +13,7 @@ export interface IAttachmentService { linkedUserId: string, ): Promise>; - syncAttachments( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IAttachmentMapper { @@ -34,5 +32,7 @@ export interface IAttachmentMapper { slug: string; remote_id: string; }[], - ): Promise; + ): Promise< + UnifiedAccountingAttachmentOutput | UnifiedAccountingAttachmentOutput[] + >; } diff --git a/packages/api/src/accounting/attachment/types/model.unified.ts b/packages/api/src/accounting/attachment/types/model.unified.ts index 6f654f503..fb1e1945c 100644 --- a/packages/api/src/accounting/attachment/types/model.unified.ts +++ b/packages/api/src/accounting/attachment/types/model.unified.ts @@ -1,3 +1,110 @@ -export class UnifiedAccountingAttachmentInput {} +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsUrl, + IsDateString, +} from 'class-validator'; -export class UnifiedAccountingAttachmentOutput extends UnifiedAccountingAttachmentInput {} +export class UnifiedAccountingAttachmentInput { + @ApiPropertyOptional({ + type: String, + example: 'invoice.pdf', + nullable: true, + description: 'The name of the attached file', + }) + @IsString() + @IsOptional() + file_name?: string; + + @ApiPropertyOptional({ + type: String, + example: 'https://example.com/files/invoice.pdf', + nullable: true, + description: 'The URL where the file can be accessed', + }) + @IsUrl() + @IsOptional() + file_url?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated account', + }) + @IsUUID() + @IsOptional() + account_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingAttachmentOutput extends UnifiedAccountingAttachmentInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the attachment record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'attachment_1234', + nullable: true, + description: + 'The remote ID of the attachment in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the attachment in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the attachment record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the attachment record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} diff --git a/packages/api/src/accounting/balancesheet/balancesheet.controller.ts b/packages/api/src/accounting/balancesheet/balancesheet.controller.ts index 74fd07ae6..a41d11aa5 100644 --- a/packages/api/src/accounting/balancesheet/balancesheet.controller.ts +++ b/packages/api/src/accounting/balancesheet/balancesheet.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -28,8 +30,10 @@ import { import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { QueryDto } from '@@core/utils/dtos/query.dto'; -import { ApiGetCustomResponse, ApiPaginatedResponse } from '@@core/utils/dtos/openapi.respone.dto'; - +import { + ApiGetCustomResponse, + ApiPaginatedResponse, +} from '@@core/utils/dtos/openapi.respone.dto'; @ApiTags('accounting/balancesheets') @Controller('accounting/balancesheets') @@ -54,6 +58,7 @@ export class BalanceSheetController { }) @ApiPaginatedResponse(UnifiedAccountingBalancesheetOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getBalanceSheets( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/accounting/balancesheet/services/balancesheet.service.ts b/packages/api/src/accounting/balancesheet/services/balancesheet.service.ts index 29787130b..207b0f5f3 100644 --- a/packages/api/src/accounting/balancesheet/services/balancesheet.service.ts +++ b/packages/api/src/accounting/balancesheet/services/balancesheet.service.ts @@ -1,20 +1,12 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { - UnifiedAccountingBalancesheetInput, - UnifiedAccountingBalancesheetOutput, -} from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; +import { UnifiedAccountingBalancesheetOutput } from '../types/model.unified'; import { ServiceRegistry } from './registry.service'; -import { OriginalBalanceSheetOutput } from '@@core/utils/types/original/original.accounting'; - -import { IBalanceSheetService } from '../types'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class BalanceSheetService { @@ -29,14 +21,97 @@ export class BalanceSheetService { } async getBalanceSheet( - id_balancesheeting_balancesheet: string, + id_acc_balance_sheet: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const balanceSheet = await this.prisma.acc_balance_sheets.findUnique({ + where: { id_acc_balance_sheet: id_acc_balance_sheet }, + }); + + if (!balanceSheet) { + throw new Error( + `Balance sheet with ID ${id_acc_balance_sheet} not found.`, + ); + } + + const lineItems = + await this.prisma.acc_balance_sheets_report_items.findMany({ + where: { id_acc_company_info: balanceSheet.id_acc_company_info }, + }); + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: balanceSheet.id_acc_balance_sheet }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedBalanceSheet: UnifiedAccountingBalancesheetOutput = { + id: balanceSheet.id_acc_balance_sheet, + name: balanceSheet.name, + currency: balanceSheet.currency as CurrencyCode, + company_info_id: balanceSheet.id_acc_company_info, + date: balanceSheet.date, + net_assets: balanceSheet.net_assets + ? Number(balanceSheet.net_assets) + : undefined, + assets: balanceSheet.assets, + liabilities: balanceSheet.liabilities, + equity: balanceSheet.equity, + remote_generated_at: balanceSheet.remote_generated_at, + field_mappings: field_mappings, + remote_id: balanceSheet.remote_id, + created_at: balanceSheet.created_at, + modified_at: balanceSheet.modified_at, + line_items: lineItems.map((item) => ({ + name: item.name, + value: item.value ? Number(item.value) : undefined, + parent_item: item.parent_item, + company_info_id: item.id_acc_company_info, + remote_id: item.remote_id, + created_at: item.created_at, + modified_at: item.modified_at, + })), + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: balanceSheet.id_acc_balance_sheet }, + }); + unifiedBalanceSheet.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.balance_sheet.pull', + method: 'GET', + url: '/accounting/balance_sheet', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedBalanceSheet; + } catch (error) { + throw error; + } } async getBalanceSheets( @@ -47,7 +122,106 @@ export class BalanceSheetService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingBalancesheetOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const balanceSheets = await this.prisma.acc_balance_sheets.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_balance_sheet: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = balanceSheets.length > limit; + if (hasNextPage) balanceSheets.pop(); + + const unifiedBalanceSheets = await Promise.all( + balanceSheets.map(async (balanceSheet) => { + const lineItems = + await this.prisma.acc_balance_sheets_report_items.findMany({ + where: { id_acc_company_info: balanceSheet.id_acc_company_info }, + }); + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: balanceSheet.id_acc_balance_sheet }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedBalanceSheet: UnifiedAccountingBalancesheetOutput = { + id: balanceSheet.id_acc_balance_sheet, + name: balanceSheet.name, + currency: balanceSheet.currency as CurrencyCode, + company_info_id: balanceSheet.id_acc_company_info, + date: balanceSheet.date, + net_assets: balanceSheet.net_assets + ? Number(balanceSheet.net_assets) + : undefined, + assets: balanceSheet.assets, + liabilities: balanceSheet.liabilities, + equity: balanceSheet.equity, + remote_generated_at: balanceSheet.remote_generated_at, + field_mappings: field_mappings, + remote_id: balanceSheet.remote_id, + created_at: balanceSheet.created_at, + modified_at: balanceSheet.modified_at, + line_items: lineItems.map((item) => ({ + name: item.name, + value: item.value ? Number(item.value) : undefined, + parent_item: item.parent_item, + company_info_id: item.id_acc_company_info, + remote_id: item.remote_id, + created_at: item.created_at, + modified_at: item.modified_at, + })), + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: balanceSheet.id_acc_balance_sheet }, + }); + unifiedBalanceSheet.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedBalanceSheet; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.balance_sheet.pull', + method: 'GET', + url: '/accounting/balance_sheets', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedBalanceSheets, + next_cursor: hasNextPage + ? balanceSheets[balanceSheets.length - 1].id_acc_balance_sheet + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/balancesheet/sync/sync.service.ts b/packages/api/src/accounting/balancesheet/sync/sync.service.ts index 5744d9ba7..8ed1ba82d 100644 --- a/packages/api/src/accounting/balancesheet/sync/sync.service.ts +++ b/packages/api/src/accounting/balancesheet/sync/sync.service.ts @@ -1,15 +1,22 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { OriginalBalanceSheetOutput } from '@@core/utils/types/original/original.accounting'; +import { LineItem } from '@accounting/cashflowstatement/types/model.unified'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_balance_sheets as AccBalanceSheet } from '@prisma/client'; import { v4 as uuidv4 } from 'uuid'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { UnifiedAccountingBalancesheetOutput } from '../types/model.unified'; import { IBalanceSheetService } from '../types'; -import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { UnifiedAccountingBalancesheetOutput } from '../types/model.unified'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -19,22 +26,203 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'balancesheet', this); } async onModuleInit() { - // Initialization logic + // Initialization logic if needed + } + + @Cron('0 */12 * * *') // every 12 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting balance sheets...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IBalanceSheetService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingBalancesheetOutput, + OriginalBalanceSheetOutput, + IBalanceSheetService + >(integrationId, linkedUserId, 'accounting', 'balancesheet', service, []); + } catch (error) { + throw error; + } } - saveToDb( + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + balanceSheets: UnifiedAccountingBalancesheetOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); + ): Promise { + try { + const balanceSheetResults: AccBalanceSheet[] = []; + + for (let i = 0; i < balanceSheets.length; i++) { + const balanceSheet = balanceSheets[i]; + const originId = balanceSheet.remote_id; + + let existingBalanceSheet = + await this.prisma.acc_balance_sheets.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const balanceSheetData = { + name: balanceSheet.name, + currency: balanceSheet.currency as CurrencyCode, + id_acc_company_info: balanceSheet.company_info_id, + date: balanceSheet.date, + net_assets: balanceSheet.net_assets + ? Number(balanceSheet.net_assets) + : null, + assets: balanceSheet.assets, + liabilities: balanceSheet.liabilities, + equity: balanceSheet.equity, + remote_generated_at: balanceSheet.remote_generated_at, + remote_id: originId, + modified_at: new Date(), + }; + + if (existingBalanceSheet) { + existingBalanceSheet = await this.prisma.acc_balance_sheets.update({ + where: { + id_acc_balance_sheet: existingBalanceSheet.id_acc_balance_sheet, + }, + data: balanceSheetData, + }); + } else { + existingBalanceSheet = await this.prisma.acc_balance_sheets.create({ + data: { + ...balanceSheetData, + id_acc_balance_sheet: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + balanceSheetResults.push(existingBalanceSheet); + + // Process field mappings + await this.ingestService.processFieldMappings( + balanceSheet.field_mappings, + existingBalanceSheet.id_acc_balance_sheet, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingBalanceSheet.id_acc_balance_sheet, + remote_data[i], + ); + + // Handle report items + if (balanceSheet.line_items && balanceSheet.line_items.length > 0) { + await this.processBalanceSheetReportItems( + balanceSheet.line_items, + existingBalanceSheet.id_acc_balance_sheet, + ); + } + } + + return balanceSheetResults; + } catch (error) { + throw error; + } + } + + private async processBalanceSheetReportItems( + lineItems: LineItem[], + balanceSheetId: string, + ): Promise { + for (const lineItem of lineItems) { + const lineItemData = { + name: lineItem.name, + value: lineItem.value ? Number(lineItem.value) : null, + parent_item: lineItem.parent_item, + id_acc_company_info: lineItem.company_info_id, + remote_id: lineItem.remote_id, + modified_at: new Date(), + }; + + const existingReportItem = + await this.prisma.acc_balance_sheets_report_items.findFirst({ + where: { + remote_id: lineItem.remote_id, + }, + }); + + if (existingReportItem) { + await this.prisma.acc_balance_sheets_report_items.update({ + where: { + id_acc_balance_sheets_report_item: + existingReportItem.id_acc_balance_sheets_report_item, + }, + data: lineItemData, + }); + } else { + await this.prisma.acc_balance_sheets_report_items.create({ + data: { + ...lineItemData, + id_acc_balance_sheets_report_item: uuidv4(), + created_at: new Date(), + }, + }); + } + } + + // Remove any existing report items that are not in the current set + const currentRemoteIds = lineItems.map((item) => item.remote_id); + await this.prisma.acc_balance_sheets_report_items.deleteMany({ + where: { + remote_id: { + notIn: currentRemoteIds, + }, + }, + }); } - // Additional methods and logic } diff --git a/packages/api/src/accounting/balancesheet/types/index.ts b/packages/api/src/accounting/balancesheet/types/index.ts index fb92474f6..d873ad836 100644 --- a/packages/api/src/accounting/balancesheet/types/index.ts +++ b/packages/api/src/accounting/balancesheet/types/index.ts @@ -5,6 +5,7 @@ import { } from './model.unified'; import { OriginalBalanceSheetOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IBalanceSheetService { addBalanceSheet( @@ -12,10 +13,7 @@ export interface IBalanceSheetService { linkedUserId: string, ): Promise>; - syncBalanceSheets( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IBalanceSheetMapper { @@ -34,5 +32,7 @@ export interface IBalanceSheetMapper { slug: string; remote_id: string; }[], - ): Promise; + ): Promise< + UnifiedAccountingBalancesheetOutput | UnifiedAccountingBalancesheetOutput[] + >; } diff --git a/packages/api/src/accounting/balancesheet/types/model.unified.ts b/packages/api/src/accounting/balancesheet/types/model.unified.ts index 71140028d..ba4cf4439 100644 --- a/packages/api/src/accounting/balancesheet/types/model.unified.ts +++ b/packages/api/src/accounting/balancesheet/types/model.unified.ts @@ -1,3 +1,187 @@ -export class UnifiedAccountingBalancesheetInput {} +import { CurrencyCode } from '@@core/utils/types'; +import { LineItem } from '@accounting/cashflowstatement/types/model.unified'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsNumber, + IsDateString, + IsArray, +} from 'class-validator'; -export class UnifiedAccountingBalancesheetOutput extends UnifiedAccountingBalancesheetInput {} +// todo balance sheet report items ? +export class UnifiedAccountingBalancesheetInput { + @ApiPropertyOptional({ + type: String, + example: 'Q2 2024 Balance Sheet', + nullable: true, + description: 'The name of the balance sheet', + }) + @IsString() + @IsOptional() + name?: string; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + enum: CurrencyCode, + nullable: true, + description: 'The currency used in the balance sheet', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company info', + }) + @IsUUID() + @IsOptional() + company_info_id?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-30T23:59:59Z', + nullable: true, + description: 'The date of the balance sheet', + }) + @IsDateString() + @IsOptional() + date?: Date; + + @ApiPropertyOptional({ + type: Number, + example: 1000000, + nullable: true, + description: 'The net assets value', + }) + @IsNumber() + @IsOptional() + net_assets?: number; + + @ApiPropertyOptional({ + type: [String], + example: ['Cash', 'Accounts Receivable', 'Inventory'], + nullable: true, + description: 'The list of assets', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + assets?: string[]; + + @ApiPropertyOptional({ + type: [String], + example: ['Accounts Payable', 'Long-term Debt'], + nullable: true, + description: 'The list of liabilities', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + liabilities?: string[]; + + @ApiPropertyOptional({ + type: [String], + example: ['Common Stock', 'Retained Earnings'], + nullable: true, + description: 'The list of equity items', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + equity?: string[]; + + @ApiPropertyOptional({ + type: Date, + example: '2024-07-01T12:00:00Z', + nullable: true, + description: + 'The date when the balance sheet was generated in the remote system', + }) + @IsDateString() + @IsOptional() + remote_generated_at?: Date; + + @ApiPropertyOptional({ + type: [LineItem], + description: 'The report items associated with this balance sheet', + }) + @IsArray() + @IsOptional() + line_items?: LineItem[]; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingBalancesheetOutput extends UnifiedAccountingBalancesheetInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the balance sheet record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'balancesheet_1234', + nullable: true, + description: + 'The remote ID of the balance sheet in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the balance sheet in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the balance sheet record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the balance sheet record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} diff --git a/packages/api/src/accounting/cashflowstatement/cashflowstatement.controller.ts b/packages/api/src/accounting/cashflowstatement/cashflowstatement.controller.ts index aa79c1f99..1f307004b 100644 --- a/packages/api/src/accounting/cashflowstatement/cashflowstatement.controller.ts +++ b/packages/api/src/accounting/cashflowstatement/cashflowstatement.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -28,8 +30,10 @@ import { import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { QueryDto } from '@@core/utils/dtos/query.dto'; -import { ApiGetCustomResponse, ApiPaginatedResponse } from '@@core/utils/dtos/openapi.respone.dto'; - +import { + ApiGetCustomResponse, + ApiPaginatedResponse, +} from '@@core/utils/dtos/openapi.respone.dto'; @ApiTags('accounting/cashflowstatements') @Controller('accounting/cashflowstatements') @@ -54,6 +58,7 @@ export class CashflowStatementController { }) @ApiPaginatedResponse(UnifiedAccountingCashflowstatementOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getCashflowStatements( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/accounting/cashflowstatement/services/cashflowstatement.service.ts b/packages/api/src/accounting/cashflowstatement/services/cashflowstatement.service.ts index 9a29e004a..bc7892f5c 100644 --- a/packages/api/src/accounting/cashflowstatement/services/cashflowstatement.service.ts +++ b/packages/api/src/accounting/cashflowstatement/services/cashflowstatement.service.ts @@ -1,20 +1,12 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { - UnifiedAccountingCashflowstatementInput, - UnifiedAccountingCashflowstatementOutput, -} from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; +import { UnifiedAccountingCashflowstatementOutput } from '../types/model.unified'; import { ServiceRegistry } from './registry.service'; -import { OriginalCashflowStatementOutput } from '@@core/utils/types/original/original.accounting'; - -import { ICashflowStatementService } from '../types'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class CashflowStatementService { @@ -29,14 +21,107 @@ export class CashflowStatementService { } async getCashflowStatement( - id_cashflowstatementing_cashflowstatement: string, + id_acc_cash_flow_statement: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const cashFlowStatement = + await this.prisma.acc_cash_flow_statements.findUnique({ + where: { id_acc_cash_flow_statement: id_acc_cash_flow_statement }, + }); + + if (!cashFlowStatement) { + throw new Error( + `Cash flow statement with ID ${id_acc_cash_flow_statement} not found.`, + ); + } + + const lineItems = + await this.prisma.acc_cash_flow_statement_report_items.findMany({ + where: { id_acc_cash_flow_statement: id_acc_cash_flow_statement }, + }); + + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: cashFlowStatement.id_acc_cash_flow_statement, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedCashFlowStatement: UnifiedAccountingCashflowstatementOutput = + { + id: cashFlowStatement.id_acc_cash_flow_statement, + name: cashFlowStatement.name, + currency: cashFlowStatement.currency as CurrencyCode, + company_id: cashFlowStatement.company, + start_period: cashFlowStatement.start_period, + end_period: cashFlowStatement.end_period, + cash_at_beginning_of_period: + cashFlowStatement.cash_at_beginning_of_period + ? Number(cashFlowStatement.cash_at_beginning_of_period) + : undefined, + cash_at_end_of_period: cashFlowStatement.cash_at_end_of_period + ? Number(cashFlowStatement.cash_at_end_of_period) + : undefined, + remote_generated_at: cashFlowStatement.remote_generated_at, + field_mappings: field_mappings, + remote_id: cashFlowStatement.remote_id, + created_at: cashFlowStatement.created_at, + modified_at: cashFlowStatement.modified_at, + line_items: lineItems.map((item) => ({ + id: item.id_acc_cash_flow_statement_report_item, + name: item.name, + value: item.value ? Number(item.value) : undefined, + type: item.type, + parent_item: item.parent_item, + remote_id: item.remote_id, + remote_generated_at: item.remote_generated_at, + created_at: item.created_at, + modified_at: item.modified_at, + })), + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: cashFlowStatement.id_acc_cash_flow_statement, + }, + }); + unifiedCashFlowStatement.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.cashflow_statement.pull', + method: 'GET', + url: '/accounting/cashflow_statement', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedCashFlowStatement; + } catch (error) { + throw error; + } } async getCashflowStatements( @@ -47,7 +132,122 @@ export class CashflowStatementService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingCashflowstatementOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const cashFlowStatements = + await this.prisma.acc_cash_flow_statements.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_cash_flow_statement: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = cashFlowStatements.length > limit; + if (hasNextPage) cashFlowStatements.pop(); + + const unifiedCashFlowStatements = await Promise.all( + cashFlowStatements.map(async (cashFlowStatement) => { + const lineItems = + await this.prisma.acc_cash_flow_statement_report_items.findMany({ + where: { + id_acc_cash_flow_statement: + cashFlowStatement.id_acc_cash_flow_statement, + }, + }); + + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: + cashFlowStatement.id_acc_cash_flow_statement, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedCashFlowStatement: UnifiedAccountingCashflowstatementOutput = + { + id: cashFlowStatement.id_acc_cash_flow_statement, + name: cashFlowStatement.name, + currency: cashFlowStatement.currency as CurrencyCode, + company_id: cashFlowStatement.company, + start_period: cashFlowStatement.start_period, + end_period: cashFlowStatement.end_period, + cash_at_beginning_of_period: + cashFlowStatement.cash_at_beginning_of_period + ? Number(cashFlowStatement.cash_at_beginning_of_period) + : undefined, + cash_at_end_of_period: cashFlowStatement.cash_at_end_of_period + ? Number(cashFlowStatement.cash_at_end_of_period) + : undefined, + remote_generated_at: cashFlowStatement.remote_generated_at, + field_mappings: field_mappings, + remote_id: cashFlowStatement.remote_id, + created_at: cashFlowStatement.created_at, + modified_at: cashFlowStatement.modified_at, + line_items: lineItems.map((item) => ({ + id: item.id_acc_cash_flow_statement_report_item, + name: item.name, + value: item.value ? Number(item.value) : undefined, + type: item.type, + parent_item: item.parent_item, + remote_id: item.remote_id, + remote_generated_at: item.remote_generated_at, + created_at: item.created_at, + modified_at: item.modified_at, + })), + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: + cashFlowStatement.id_acc_cash_flow_statement, + }, + }); + unifiedCashFlowStatement.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedCashFlowStatement; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.cashflow_statement.pull', + method: 'GET', + url: '/accounting/cashflow_statements', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedCashFlowStatements, + next_cursor: hasNextPage + ? cashFlowStatements[cashFlowStatements.length - 1] + .id_acc_cash_flow_statement + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/cashflowstatement/sync/sync.service.ts b/packages/api/src/accounting/cashflowstatement/sync/sync.service.ts index a9841e4c9..a33b883de 100644 --- a/packages/api/src/accounting/cashflowstatement/sync/sync.service.ts +++ b/packages/api/src/accounting/cashflowstatement/sync/sync.service.ts @@ -1,15 +1,24 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { OriginalCashflowStatementOutput } from '@@core/utils/types/original/original.accounting'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_cash_flow_statements as AccCashFlowStatement } from '@prisma/client'; import { v4 as uuidv4 } from 'uuid'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { UnifiedAccountingCashflowstatementOutput } from '../types/model.unified'; import { ICashflowStatementService } from '../types'; -import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { + LineItem, + UnifiedAccountingCashflowstatementOutput, +} from '../types/model.unified'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -19,24 +28,222 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'cashflow_statement', this); } async onModuleInit() { - // Initialization logic + // Initialization logic if needed + } + + @Cron('0 */12 * * *') // every 12 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting cash flow statements...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: ICashflowStatementService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingCashflowstatementOutput, + OriginalCashflowStatementOutput, + ICashflowStatementService + >( + integrationId, + linkedUserId, + 'accounting', + 'cashflow_statement', + service, + [], + ); + } catch (error) { + throw error; + } } - saveToDb( + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + cashFlowStatements: UnifiedAccountingCashflowstatementOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); + ): Promise { + try { + const cashFlowStatementResults: AccCashFlowStatement[] = []; + + for (let i = 0; i < cashFlowStatements.length; i++) { + const cashFlowStatement = cashFlowStatements[i]; + const originId = cashFlowStatement.remote_id; + + let existingCashFlowStatement = + await this.prisma.acc_cash_flow_statements.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const cashFlowStatementData = { + name: cashFlowStatement.name, + currency: cashFlowStatement.currency as CurrencyCode, + company: cashFlowStatement.company_id, + start_period: cashFlowStatement.start_period, + end_period: cashFlowStatement.end_period, + cash_at_beginning_of_period: + cashFlowStatement.cash_at_beginning_of_period + ? Number(cashFlowStatement.cash_at_beginning_of_period) + : null, + cash_at_end_of_period: cashFlowStatement.cash_at_end_of_period + ? Number(cashFlowStatement.cash_at_end_of_period) + : null, + remote_generated_at: cashFlowStatement.remote_generated_at, + remote_id: originId, + modified_at: new Date(), + }; + + if (existingCashFlowStatement) { + existingCashFlowStatement = + await this.prisma.acc_cash_flow_statements.update({ + where: { + id_acc_cash_flow_statement: + existingCashFlowStatement.id_acc_cash_flow_statement, + }, + data: cashFlowStatementData, + }); + } else { + existingCashFlowStatement = + await this.prisma.acc_cash_flow_statements.create({ + data: { + ...cashFlowStatementData, + id_acc_cash_flow_statement: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + cashFlowStatementResults.push(existingCashFlowStatement); + + // Process field mappings + await this.ingestService.processFieldMappings( + cashFlowStatement.field_mappings, + existingCashFlowStatement.id_acc_cash_flow_statement, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingCashFlowStatement.id_acc_cash_flow_statement, + remote_data[i], + ); + + // Handle report items + if ( + cashFlowStatement.line_items && + cashFlowStatement.line_items.length > 0 + ) { + await this.processCashFlowStatementReportItems( + existingCashFlowStatement.id_acc_cash_flow_statement, + cashFlowStatement.line_items, + ); + } + } + + return cashFlowStatementResults; + } catch (error) { + throw error; + } } - // Additional methods and logic + private async processCashFlowStatementReportItems( + cashFlowStatementId: string, + reportItems: LineItem[], + ): Promise { + for (const reportItem of reportItems) { + const reportItemData = { + name: reportItem.name, + value: reportItem.value ? Number(reportItem.value) : null, + type: reportItem.type, + parent_item: reportItem.parent_item, + remote_generated_at: reportItem.remote_generated_at, + remote_id: reportItem.remote_id, + modified_at: new Date(), + id_acc_cash_flow_statement: cashFlowStatementId, + }; + + const existingReportItem = + await this.prisma.acc_cash_flow_statement_report_items.findFirst({ + where: { + remote_id: reportItem.remote_id, + id_acc_cash_flow_statement: cashFlowStatementId, + }, + }); + + if (existingReportItem) { + await this.prisma.acc_cash_flow_statement_report_items.update({ + where: { + id_acc_cash_flow_statement_report_item: + existingReportItem.id_acc_cash_flow_statement_report_item, + }, + data: reportItemData, + }); + } else { + await this.prisma.acc_cash_flow_statement_report_items.create({ + data: { + ...reportItemData, + id_acc_cash_flow_statement_report_item: uuidv4(), + created_at: new Date(), + }, + }); + } + } + + // Remove any existing report items that are not in the current set + const currentRemoteIds = reportItems.map((item) => item.remote_id); + await this.prisma.acc_cash_flow_statement_report_items.deleteMany({ + where: { + id_acc_cash_flow_statement: cashFlowStatementId, + remote_id: { + notIn: currentRemoteIds, + }, + }, + }); + } } diff --git a/packages/api/src/accounting/cashflowstatement/types/index.ts b/packages/api/src/accounting/cashflowstatement/types/index.ts index 4ca7daa48..93970e832 100644 --- a/packages/api/src/accounting/cashflowstatement/types/index.ts +++ b/packages/api/src/accounting/cashflowstatement/types/index.ts @@ -5,6 +5,7 @@ import { } from './model.unified'; import { OriginalCashflowStatementOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface ICashflowStatementService { addCashflowStatement( @@ -12,9 +13,8 @@ export interface ICashflowStatementService { linkedUserId: string, ): Promise>; - syncCashflowStatements( - linkedUserId: string, - custom_properties?: string[], + sync( + data: SyncParam, ): Promise>; } @@ -34,5 +34,8 @@ export interface ICashflowStatementMapper { slug: string; remote_id: string; }[], - ): Promise; + ): Promise< + | UnifiedAccountingCashflowstatementOutput + | UnifiedAccountingCashflowstatementOutput[] + >; } diff --git a/packages/api/src/accounting/cashflowstatement/types/model.unified.ts b/packages/api/src/accounting/cashflowstatement/types/model.unified.ts index 25c4e0459..9bfedd309 100644 --- a/packages/api/src/accounting/cashflowstatement/types/model.unified.ts +++ b/packages/api/src/accounting/cashflowstatement/types/model.unified.ts @@ -1,3 +1,265 @@ -export class UnifiedAccountingCashflowstatementInput {} +import { CurrencyCode } from '@@core/utils/types'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsNumber, + IsDateString, + IsArray, +} from 'class-validator'; -export class UnifiedAccountingCashflowstatementOutput extends UnifiedAccountingCashflowstatementInput {} +export class LineItem { + @ApiPropertyOptional({ + type: String, + example: 'Net Income', + nullable: true, + description: 'The name of the report item', + }) + @IsString() + @IsOptional() + name?: string; + + @ApiPropertyOptional({ + type: Number, + example: 100000, + nullable: true, + description: 'The value of the report item', + }) + @IsNumber() + @IsOptional() + value?: number; + + @ApiPropertyOptional({ + type: String, + example: 'Operating Activities', + nullable: true, + description: 'The type of the report item', + }) + @IsString() + @IsOptional() + type?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the parent item', + }) + @IsUUID() + @IsOptional() + parent_item?: string; + + @ApiPropertyOptional({ + type: String, + example: 'report_item_1234', + nullable: true, + description: 'The remote ID of the report item', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-07-01T12:00:00Z', + nullable: true, + description: + 'The date when the report item was generated in the remote system', + }) + @IsDateString() + @IsOptional() + remote_generated_at?: Date; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company info object', + }) + @IsUUID() + @IsOptional() + company_info_id?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + description: 'The created date of the report item', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + description: 'The last modified date of the report item', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} + +export class UnifiedAccountingCashflowstatementInput { + @ApiPropertyOptional({ + type: String, + example: 'Q2 2024 Cash Flow Statement', + nullable: true, + description: 'The name of the cash flow statement', + }) + @IsString() + @IsOptional() + name?: string; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + enum: CurrencyCode, + nullable: true, + description: 'The currency used in the cash flow statement', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company', + }) + @IsUUID() + @IsOptional() + company_id?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-04-01T00:00:00Z', + nullable: true, + description: + 'The start date of the period covered by the cash flow statement', + }) + @IsDateString() + @IsOptional() + start_period?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-30T23:59:59Z', + nullable: true, + description: + 'The end date of the period covered by the cash flow statement', + }) + @IsDateString() + @IsOptional() + end_period?: Date; + + @ApiPropertyOptional({ + type: Number, + example: 1000000, + nullable: true, + description: 'The cash balance at the beginning of the period', + }) + @IsNumber() + @IsOptional() + cash_at_beginning_of_period?: number; + + @ApiPropertyOptional({ + type: Number, + example: 1200000, + nullable: true, + description: 'The cash balance at the end of the period', + }) + @IsNumber() + @IsOptional() + cash_at_end_of_period?: number; + + @ApiPropertyOptional({ + type: Date, + example: '2024-07-01T12:00:00Z', + nullable: true, + description: + 'The date when the cash flow statement was generated in the remote system', + }) + @IsDateString() + @IsOptional() + remote_generated_at?: Date; + + @ApiPropertyOptional({ + type: [LineItem], + description: 'The report items associated with this cash flow statement', + }) + @IsArray() + @IsOptional() + line_items?: LineItem[]; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingCashflowstatementOutput extends UnifiedAccountingCashflowstatementInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the cash flow statement record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'cashflowstatement_1234', + nullable: true, + description: + 'The remote ID of the cash flow statement in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the cash flow statement in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the cash flow statement record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the cash flow statement record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} diff --git a/packages/api/src/accounting/companyinfo/companyinfo.controller.ts b/packages/api/src/accounting/companyinfo/companyinfo.controller.ts index 84d1a9d8c..7b45bf430 100644 --- a/packages/api/src/accounting/companyinfo/companyinfo.controller.ts +++ b/packages/api/src/accounting/companyinfo/companyinfo.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -28,8 +30,10 @@ import { import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { QueryDto } from '@@core/utils/dtos/query.dto'; -import { ApiGetCustomResponse, ApiPaginatedResponse } from '@@core/utils/dtos/openapi.respone.dto'; - +import { + ApiGetCustomResponse, + ApiPaginatedResponse, +} from '@@core/utils/dtos/openapi.respone.dto'; @ApiTags('accounting/companyinfos') @Controller('accounting/companyinfos') @@ -54,6 +58,7 @@ export class CompanyInfoController { }) @ApiPaginatedResponse(UnifiedAccountingCompanyinfoOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getCompanyInfos( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/accounting/companyinfo/services/companyinfo.service.ts b/packages/api/src/accounting/companyinfo/services/companyinfo.service.ts index e818a6f27..773f281d4 100644 --- a/packages/api/src/accounting/companyinfo/services/companyinfo.service.ts +++ b/packages/api/src/accounting/companyinfo/services/companyinfo.service.ts @@ -2,19 +2,15 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; +import { ApiResponse, CurrencyCode } from '@@core/utils/types'; import { throwTypedError } from '@@core/utils/errors'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { UnifiedAccountingCompanyinfoInput, UnifiedAccountingCompanyinfoOutput, } from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; -import { OriginalCompanyInfoOutput } from '@@core/utils/types/original/original.accounting'; - -import { ICompanyInfoService } from '../types'; @Injectable() export class CompanyInfoService { @@ -29,14 +25,81 @@ export class CompanyInfoService { } async getCompanyInfo( - id_companyinfoing_companyinfo: string, + id_acc_company_info: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const companyInfo = await this.prisma.acc_company_infos.findUnique({ + where: { id_acc_company_info: id_acc_company_info }, + }); + + if (!companyInfo) { + throw new Error( + `Company info with ID ${id_acc_company_info} not found.`, + ); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: companyInfo.id_acc_company_info }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedCompanyInfo: UnifiedAccountingCompanyinfoOutput = { + id: companyInfo.id_acc_company_info, + name: companyInfo.name, + legal_name: companyInfo.legal_name, + tax_number: companyInfo.tax_number, + fiscal_year_end_month: companyInfo.fiscal_year_end_month, + fiscal_year_end_day: companyInfo.fiscal_year_end_day, + currency: companyInfo.currency as CurrencyCode, + urls: companyInfo.urls, + tracking_categories: companyInfo.tracking_categories, + field_mappings: field_mappings, + remote_id: companyInfo.remote_id, + remote_created_at: companyInfo.remote_created_at, + created_at: companyInfo.created_at, + modified_at: companyInfo.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: companyInfo.id_acc_company_info }, + }); + unifiedCompanyInfo.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.company_info.pull', + method: 'GET', + url: '/accounting/company_info', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedCompanyInfo; + } catch (error) { + throw error; + } } async getCompanyInfos( @@ -47,7 +110,90 @@ export class CompanyInfoService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingCompanyinfoOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const companyInfos = await this.prisma.acc_company_infos.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_company_info: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = companyInfos.length > limit; + if (hasNextPage) companyInfos.pop(); + + const unifiedCompanyInfos = await Promise.all( + companyInfos.map(async (companyInfo) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: companyInfo.id_acc_company_info }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedCompanyInfo: UnifiedAccountingCompanyinfoOutput = { + id: companyInfo.id_acc_company_info, + name: companyInfo.name, + legal_name: companyInfo.legal_name, + tax_number: companyInfo.tax_number, + fiscal_year_end_month: companyInfo.fiscal_year_end_month, + fiscal_year_end_day: companyInfo.fiscal_year_end_day, + currency: companyInfo.currency as CurrencyCode, + urls: companyInfo.urls, + tracking_categories: companyInfo.tracking_categories, + field_mappings: field_mappings, + remote_id: companyInfo.remote_id, + remote_created_at: companyInfo.remote_created_at, + created_at: companyInfo.created_at, + modified_at: companyInfo.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: companyInfo.id_acc_company_info }, + }); + unifiedCompanyInfo.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedCompanyInfo; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.company_info.pull', + method: 'GET', + url: '/accounting/company_infos', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedCompanyInfos, + next_cursor: hasNextPage + ? companyInfos[companyInfos.length - 1].id_acc_company_info + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/companyinfo/sync/sync.service.ts b/packages/api/src/accounting/companyinfo/sync/sync.service.ts index fcb8224d1..5a96908fc 100644 --- a/packages/api/src/accounting/companyinfo/sync/sync.service.ts +++ b/packages/api/src/accounting/companyinfo/sync/sync.service.ts @@ -1,9 +1,8 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; - import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ApiResponse, CurrencyCode } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; @@ -11,6 +10,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedAccountingCompanyinfoOutput } from '../types/model.unified'; import { ICompanyInfoService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_company_infos as AccCompanyInfo } from '@prisma/client'; +import { OriginalCompanyInfoOutput } from '@@core/utils/types/original/original.accounting'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -20,24 +25,143 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'company_info', this); } async onModuleInit() { - // Initialization logic + // Initialization logic if needed + } + + @Cron('0 */12 * * *') // every 12 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting company info...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } } - saveToDb( + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: ICompanyInfoService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingCompanyinfoOutput, + OriginalCompanyInfoOutput, + ICompanyInfoService + >(integrationId, linkedUserId, 'accounting', 'company_info', service, []); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + companyInfos: UnifiedAccountingCompanyinfoOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const companyInfoResults: AccCompanyInfo[] = []; - // Additional methods and logic + for (let i = 0; i < companyInfos.length; i++) { + const companyInfo = companyInfos[i]; + const originId = companyInfo.remote_id; + + let existingCompanyInfo = await this.prisma.acc_company_infos.findFirst( + { + where: { + remote_id: originId, + id_connection: connection_id, + }, + }, + ); + + const companyInfoData = { + name: companyInfo.name, + legal_name: companyInfo.legal_name, + tax_number: companyInfo.tax_number, + fiscal_year_end_month: companyInfo.fiscal_year_end_month, + fiscal_year_end_day: companyInfo.fiscal_year_end_day, + currency: companyInfo.currency as CurrencyCode, + urls: companyInfo.urls, + tracking_categories: companyInfo.tracking_categories, + remote_created_at: companyInfo.remote_created_at, + remote_id: originId, + modified_at: new Date(), + }; + + if (existingCompanyInfo) { + existingCompanyInfo = await this.prisma.acc_company_infos.update({ + where: { + id_acc_company_info: existingCompanyInfo.id_acc_company_info, + }, + data: companyInfoData, + }); + } else { + existingCompanyInfo = await this.prisma.acc_company_infos.create({ + data: { + ...companyInfoData, + id_acc_company_info: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + companyInfoResults.push(existingCompanyInfo); + + // Process field mappings + await this.ingestService.processFieldMappings( + companyInfo.field_mappings, + existingCompanyInfo.id_acc_company_info, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingCompanyInfo.id_acc_company_info, + remote_data[i], + ); + } + + return companyInfoResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/accounting/companyinfo/types/index.ts b/packages/api/src/accounting/companyinfo/types/index.ts index 3f260960c..e54504ade 100644 --- a/packages/api/src/accounting/companyinfo/types/index.ts +++ b/packages/api/src/accounting/companyinfo/types/index.ts @@ -5,6 +5,7 @@ import { } from './model.unified'; import { OriginalCompanyInfoOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface ICompanyInfoService { addCompanyInfo( @@ -12,10 +13,7 @@ export interface ICompanyInfoService { linkedUserId: string, ): Promise>; - syncCompanyInfos( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface ICompanyInfoMapper { @@ -34,5 +32,7 @@ export interface ICompanyInfoMapper { slug: string; remote_id: string; }[], - ): Promise; + ): Promise< + UnifiedAccountingCompanyinfoOutput | UnifiedAccountingCompanyinfoOutput[] + >; } diff --git a/packages/api/src/accounting/companyinfo/types/model.unified.ts b/packages/api/src/accounting/companyinfo/types/model.unified.ts index a54ca447c..1474454b5 100644 --- a/packages/api/src/accounting/companyinfo/types/model.unified.ts +++ b/packages/api/src/accounting/companyinfo/types/model.unified.ts @@ -1,3 +1,186 @@ -export class UnifiedAccountingCompanyinfoInput {} +import { CurrencyCode } from '@@core/utils/types'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsNumber, + IsDateString, + IsArray, + IsUrl, + Min, + Max, +} from 'class-validator'; -export class UnifiedAccountingCompanyinfoOutput extends UnifiedAccountingCompanyinfoInput {} +export class UnifiedAccountingCompanyinfoInput { + @ApiPropertyOptional({ + type: String, + example: 'Acme Corporation', + nullable: true, + description: 'The name of the company', + }) + @IsString() + @IsOptional() + name?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Acme Corporation LLC', + nullable: true, + description: 'The legal name of the company', + }) + @IsString() + @IsOptional() + legal_name?: string; + + @ApiPropertyOptional({ + type: String, + example: '123456789', + nullable: true, + description: 'The tax number of the company', + }) + @IsString() + @IsOptional() + tax_number?: string; + + @ApiPropertyOptional({ + type: Number, + example: 12, + nullable: true, + description: 'The month of the fiscal year end (1-12)', + }) + @IsNumber() + @Min(1) + @Max(12) + @IsOptional() + fiscal_year_end_month?: number; + + @ApiPropertyOptional({ + type: Number, + example: 31, + nullable: true, + description: 'The day of the fiscal year end (1-31)', + }) + @IsNumber() + @Min(1) + @Max(31) + @IsOptional() + fiscal_year_end_day?: number; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + enum: CurrencyCode, + nullable: true, + description: 'The currency used by the company', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: [String], + example: ['https://www.acmecorp.com', 'https://store.acmecorp.com'], + nullable: true, + description: 'The URLs associated with the company', + }) + @IsArray() + @IsUrl({}, { each: true }) + @IsOptional() + urls?: string[]; + + @ApiPropertyOptional({ + type: [String], + example: [ + '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + ], + nullable: true, + description: 'The UUIDs of the tracking categories used by the company', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tracking_categories?: string[]; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingCompanyinfoOutput extends UnifiedAccountingCompanyinfoInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the company info record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'company_1234', + nullable: true, + description: + 'The remote ID of the company info in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the company info in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: + 'The date when the company info was created in the remote system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the company info record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the company info record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} diff --git a/packages/api/src/accounting/contact/contact.controller.ts b/packages/api/src/accounting/contact/contact.controller.ts index a6e3482fa..12fbf1b3a 100644 --- a/packages/api/src/accounting/contact/contact.controller.ts +++ b/packages/api/src/accounting/contact/contact.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -34,7 +36,6 @@ import { ApiPostCustomResponse, } from '@@core/utils/dtos/openapi.respone.dto'; - @ApiTags('accounting/contacts') @Controller('accounting/contacts') export class ContactController { @@ -58,6 +59,7 @@ export class ContactController { }) @ApiPaginatedResponse(UnifiedAccountingContactOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getContacts( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/accounting/contact/services/contact.service.ts b/packages/api/src/accounting/contact/services/contact.service.ts index e96a760d5..1d87d1c26 100644 --- a/packages/api/src/accounting/contact/services/contact.service.ts +++ b/packages/api/src/accounting/contact/services/contact.service.ts @@ -1,20 +1,15 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { CurrencyCode } from '@@core/utils/types'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; import { UnifiedAccountingContactInput, UnifiedAccountingContactOutput, } from '../types/model.unified'; - -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; -import { OriginalContactOutput } from '@@core/utils/types/original/original.accounting'; - -import { IContactService } from '../types'; @Injectable() export class ContactService { @@ -36,18 +31,111 @@ export class ContactService { linkedUserId: string, remote_data?: boolean, ): Promise { - return; + try { + const service = this.serviceRegistry.getService(integrationId); + const resp = await service.addContact(unifiedContactData, linkedUserId); + + const savedContact = await this.prisma.acc_contacts.create({ + data: { + id_acc_contact: uuidv4(), + ...unifiedContactData, + remote_id: resp.data.remote_id, + id_connection: connection_id, + created_at: new Date(), + modified_at: new Date(), + }, + }); + + const result: UnifiedAccountingContactOutput = { + ...savedContact, + currency: savedContact.currency as CurrencyCode, + id: savedContact.id_acc_contact, + }; + + if (remote_data) { + result.remote_data = resp.data; + } + + return result; + } catch (error) { + throw error; + } } async getContact( - id_contacting_contact: string, + id_acc_contact: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const contact = await this.prisma.acc_contacts.findUnique({ + where: { id_acc_contact: id_acc_contact }, + }); + + if (!contact) { + throw new Error(`Contact with ID ${id_acc_contact} not found.`); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: contact.id_acc_contact }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedContact: UnifiedAccountingContactOutput = { + id: contact.id_acc_contact, + name: contact.name, + is_supplier: contact.is_supplier, + is_customer: contact.is_customer, + email_address: contact.email_address, + tax_number: contact.tax_number, + status: contact.status, + currency: contact.currency as CurrencyCode, + remote_updated_at: contact.remote_updated_at || null, + company_info_id: contact.id_acc_company_info, + field_mappings: field_mappings, + remote_id: contact.remote_id, + created_at: contact.created_at, + modified_at: contact.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: contact.id_acc_contact }, + }); + unifiedContact.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.contact.pull', + method: 'GET', + url: '/accounting/contact', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedContact; + } catch (error) { + throw error; + } } async getContacts( @@ -58,7 +146,90 @@ export class ContactService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingContactOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const contacts = await this.prisma.acc_contacts.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_contact: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = contacts.length > limit; + if (hasNextPage) contacts.pop(); + + const unifiedContacts = await Promise.all( + contacts.map(async (contact) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: contact.id_acc_contact }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedContact: UnifiedAccountingContactOutput = { + id: contact.id_acc_contact, + name: contact.name, + is_supplier: contact.is_supplier, + is_customer: contact.is_customer, + email_address: contact.email_address, + tax_number: contact.tax_number, + status: contact.status, + currency: contact.currency as CurrencyCode, + remote_updated_at: contact.remote_updated_at || null, + company_info_id: contact.id_acc_company_info, + field_mappings: field_mappings, + remote_id: contact.remote_id, + created_at: contact.created_at, + modified_at: contact.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: contact.id_acc_contact }, + }); + unifiedContact.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedContact; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.contact.pull', + method: 'GET', + url: '/accounting/contacts', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedContacts, + next_cursor: hasNextPage + ? contacts[contacts.length - 1].id_acc_contact + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/contact/sync/sync.service.ts b/packages/api/src/accounting/contact/sync/sync.service.ts index 2bb5fcfb2..f3e6a1919 100644 --- a/packages/api/src/accounting/contact/sync/sync.service.ts +++ b/packages/api/src/accounting/contact/sync/sync.service.ts @@ -1,9 +1,8 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; - import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ApiResponse, CurrencyCode } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; @@ -11,6 +10,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedAccountingContactOutput } from '../types/model.unified'; import { IContactService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_contacts as AccContact } from '@prisma/client'; +import { OriginalContactOutput } from '@@core/utils/types/original/original.accounting'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -20,24 +25,139 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'contact', this); } async onModuleInit() { - // Initialization logic + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting contacts...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } } - saveToDb( + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IContactService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingContactOutput, + OriginalContactOutput, + IContactService + >(integrationId, linkedUserId, 'accounting', 'contact', service, []); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + contacts: UnifiedAccountingContactOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const contactResults: AccContact[] = []; - // Additional methods and logic + for (let i = 0; i < contacts.length; i++) { + const contact = contacts[i]; + const originId = contact.remote_id; + + let existingContact = await this.prisma.acc_contacts.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const contactData = { + name: contact.name, + is_supplier: contact.is_supplier, + is_customer: contact.is_customer, + email_address: contact.email_address, + tax_number: contact.tax_number, + status: contact.status, + currency: contact.currency as CurrencyCode, + remote_updated_at: contact.remote_updated_at, + id_acc_company_info: contact.company_info_id, + remote_id: originId, + modified_at: new Date(), + }; + + if (existingContact) { + existingContact = await this.prisma.acc_contacts.update({ + where: { id_acc_contact: existingContact.id_acc_contact }, + data: contactData, + }); + } else { + existingContact = await this.prisma.acc_contacts.create({ + data: { + ...contactData, + id_acc_contact: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + contactResults.push(existingContact); + + // Process field mappings + await this.ingestService.processFieldMappings( + contact.field_mappings, + existingContact.id_acc_contact, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingContact.id_acc_contact, + remote_data[i], + ); + } + + return contactResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/accounting/contact/types/index.ts b/packages/api/src/accounting/contact/types/index.ts index d6cb71f9c..a9c26c209 100644 --- a/packages/api/src/accounting/contact/types/index.ts +++ b/packages/api/src/accounting/contact/types/index.ts @@ -1,7 +1,11 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { UnifiedAccountingContactInput, UnifiedAccountingContactOutput } from './model.unified'; +import { + UnifiedAccountingContactInput, + UnifiedAccountingContactOutput, +} from './model.unified'; import { OriginalContactOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IContactService { addContact( @@ -9,10 +13,7 @@ export interface IContactService { linkedUserId: string, ): Promise>; - syncContacts( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IContactMapper { diff --git a/packages/api/src/accounting/contact/types/model.unified.ts b/packages/api/src/accounting/contact/types/model.unified.ts index 28bb89a80..6bce88f18 100644 --- a/packages/api/src/accounting/contact/types/model.unified.ts +++ b/packages/api/src/accounting/contact/types/model.unified.ts @@ -1,3 +1,173 @@ -export class UnifiedAccountingContactInput {} +import { CurrencyCode } from '@@core/utils/types'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsBoolean, + IsEmail, + IsDateString, +} from 'class-validator'; -export class UnifiedAccountingContactOutput extends UnifiedAccountingContactInput {} +export class UnifiedAccountingContactInput { + @ApiPropertyOptional({ + type: String, + example: 'John Doe', + nullable: true, + description: 'The name of the contact', + }) + @IsString() + @IsOptional() + name?: string; + + @ApiPropertyOptional({ + type: Boolean, + example: true, + nullable: true, + description: 'Indicates if the contact is a supplier', + }) + @IsBoolean() + @IsOptional() + is_supplier?: boolean; + + @ApiPropertyOptional({ + type: Boolean, + example: false, + nullable: true, + description: 'Indicates if the contact is a customer', + }) + @IsBoolean() + @IsOptional() + is_customer?: boolean; + + @ApiPropertyOptional({ + type: String, + example: 'john.doe@example.com', + nullable: true, + description: 'The email address of the contact', + }) + @IsEmail() + @IsOptional() + email_address?: string; + + @ApiPropertyOptional({ + type: String, + example: '123456789', + nullable: true, + description: 'The tax number of the contact', + }) + @IsString() + @IsOptional() + tax_number?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Active', + nullable: true, + description: 'The status of the contact', + }) + @IsString() + @IsOptional() + status?: string; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + nullable: true, + enum: CurrencyCode, + description: 'The currency associated with the contact', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: String, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: + 'The date when the contact was last updated in the remote system', + }) + @IsDateString() + @IsOptional() + remote_updated_at?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company info', + }) + @IsUUID() + @IsOptional() + company_info_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingContactOutput extends UnifiedAccountingContactInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the contact record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'contact_1234', + nullable: true, + description: 'The remote ID of the contact in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the contact in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the contact record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the contact record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} diff --git a/packages/api/src/accounting/creditnote/creditnote.controller.ts b/packages/api/src/accounting/creditnote/creditnote.controller.ts index f17841e3b..14a5441bc 100644 --- a/packages/api/src/accounting/creditnote/creditnote.controller.ts +++ b/packages/api/src/accounting/creditnote/creditnote.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -33,7 +35,6 @@ import { ApiPaginatedResponse, } from '@@core/utils/dtos/openapi.respone.dto'; - @ApiTags('accounting/creditnotes') @Controller('accounting/creditnotes') export class CreditNoteController { @@ -57,6 +58,7 @@ export class CreditNoteController { }) @ApiPaginatedResponse(UnifiedAccountingCreditnoteOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getCreditNotes( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/accounting/creditnote/services/creditnote.service.ts b/packages/api/src/accounting/creditnote/services/creditnote.service.ts index a5e719772..d2b6dc4ac 100644 --- a/packages/api/src/accounting/creditnote/services/creditnote.service.ts +++ b/packages/api/src/accounting/creditnote/services/creditnote.service.ts @@ -2,19 +2,15 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; +import { ApiResponse, CurrencyCode } from '@@core/utils/types'; import { throwTypedError } from '@@core/utils/errors'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { UnifiedAccountingCreditnoteInput, UnifiedAccountingCreditnoteOutput, } from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; -import { OriginalCreditNoteOutput } from '@@core/utils/types/original/original.accounting'; - -import { ICreditNoteService } from '../types'; @Injectable() export class CreditNoteService { @@ -29,14 +25,89 @@ export class CreditNoteService { } async getCreditNote( - id_creditnoteing_creditnote: string, + id_acc_credit_note: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const creditNote = await this.prisma.acc_credit_notes.findUnique({ + where: { id_acc_credit_note: id_acc_credit_note }, + }); + + if (!creditNote) { + throw new Error(`Credit note with ID ${id_acc_credit_note} not found.`); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: creditNote.id_acc_credit_note }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedCreditNote: UnifiedAccountingCreditnoteOutput = { + id: creditNote.id_acc_credit_note, + transaction_date: creditNote.transaction_date?.toISOString(), + status: creditNote.status, + number: creditNote.number, + contact_id: creditNote.id_acc_contact, + company_id: creditNote.company, + exchange_rate: creditNote.exchange_rate, + total_amount: creditNote.total_amount + ? Number(creditNote.total_amount) + : undefined, + remaining_credit: creditNote.remaining_credit + ? Number(creditNote.remaining_credit) + : undefined, + tracking_categories: creditNote.tracking_categories, + currency: creditNote.currency as CurrencyCode, + payments: creditNote.payments, + applied_payments: creditNote.applied_payments, + accounting_period_id: creditNote.id_acc_accounting_period, + field_mappings: field_mappings, + remote_id: creditNote.remote_id, + remote_created_at: creditNote.remote_created_at, + remote_updated_at: creditNote.remote_updated_at, + created_at: creditNote.created_at, + modified_at: creditNote.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: creditNote.id_acc_credit_note }, + }); + unifiedCreditNote.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.credit_note.pull', + method: 'GET', + url: '/accounting/credit_note', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedCreditNote; + } catch (error) { + throw error; + } } async getCreditNotes( @@ -47,7 +118,100 @@ export class CreditNoteService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingCreditnoteOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const creditNotes = await this.prisma.acc_credit_notes.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_credit_note: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = creditNotes.length > limit; + if (hasNextPage) creditNotes.pop(); + + const unifiedCreditNotes = await Promise.all( + creditNotes.map(async (creditNote) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: creditNote.id_acc_credit_note }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedCreditNote: UnifiedAccountingCreditnoteOutput = { + id: creditNote.id_acc_credit_note, + transaction_date: creditNote.transaction_date?.toISOString(), + status: creditNote.status, + number: creditNote.number, + contact_id: creditNote.id_acc_contact, + company_id: creditNote.company, + exchange_rate: creditNote.exchange_rate, + total_amount: creditNote.total_amount + ? Number(creditNote.total_amount) + : undefined, + remaining_credit: creditNote.remaining_credit + ? Number(creditNote.remaining_credit) + : undefined, + tracking_categories: creditNote.tracking_categories, + currency: creditNote.currency as CurrencyCode, + payments: creditNote.payments, + applied_payments: creditNote.applied_payments, + accounting_period_id: creditNote.id_acc_accounting_period, + field_mappings: field_mappings, + remote_id: creditNote.remote_id, + remote_created_at: creditNote.remote_created_at, + remote_updated_at: creditNote.remote_updated_at, + created_at: creditNote.created_at, + modified_at: creditNote.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: creditNote.id_acc_credit_note }, + }); + unifiedCreditNote.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedCreditNote; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.credit_note.pull', + method: 'GET', + url: '/accounting/credit_notes', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedCreditNotes, + next_cursor: hasNextPage + ? creditNotes[creditNotes.length - 1].id_acc_credit_note + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/creditnote/sync/sync.service.ts b/packages/api/src/accounting/creditnote/sync/sync.service.ts index 79780ff69..e26428023 100644 --- a/packages/api/src/accounting/creditnote/sync/sync.service.ts +++ b/packages/api/src/accounting/creditnote/sync/sync.service.ts @@ -1,9 +1,8 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; - import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ApiResponse, CurrencyCode } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; @@ -11,6 +10,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedAccountingCreditnoteOutput } from '../types/model.unified'; import { ICreditNoteService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_credit_notes as AccCreditNote } from '@prisma/client'; +import { OriginalCreditNoteOutput } from '@@core/utils/types/original/original.accounting'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -20,24 +25,153 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'credit_note', this); } async onModuleInit() { - // Initialization logic + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting credit notes...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } } - saveToDb( + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: ICreditNoteService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingCreditnoteOutput, + OriginalCreditNoteOutput, + ICreditNoteService + >(integrationId, linkedUserId, 'accounting', 'credit_note', service, []); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + creditNotes: UnifiedAccountingCreditnoteOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const creditNoteResults: AccCreditNote[] = []; - // Additional methods and logic + for (let i = 0; i < creditNotes.length; i++) { + const creditNote = creditNotes[i]; + const originId = creditNote.remote_id; + + let existingCreditNote = await this.prisma.acc_credit_notes.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const creditNoteData = { + transaction_date: creditNote.transaction_date + ? new Date(creditNote.transaction_date) + : null, + status: creditNote.status, + number: creditNote.number, + id_acc_contact: creditNote.contact_id, + company: creditNote.company_id, + exchange_rate: creditNote.exchange_rate, + total_amount: creditNote.total_amount + ? Number(creditNote.total_amount) + : null, + remaining_credit: creditNote.remaining_credit + ? Number(creditNote.remaining_credit) + : null, + tracking_categories: creditNote.tracking_categories, + currency: creditNote.currency as CurrencyCode, + payments: creditNote.payments, + applied_payments: creditNote.applied_payments, + id_acc_accounting_period: creditNote.accounting_period_id, + remote_id: originId, + remote_created_at: creditNote.remote_created_at, + remote_updated_at: creditNote.remote_updated_at, + modified_at: new Date(), + }; + + if (existingCreditNote) { + existingCreditNote = await this.prisma.acc_credit_notes.update({ + where: { + id_acc_credit_note: existingCreditNote.id_acc_credit_note, + }, + data: creditNoteData, + }); + } else { + existingCreditNote = await this.prisma.acc_credit_notes.create({ + data: { + ...creditNoteData, + id_acc_credit_note: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + creditNoteResults.push(existingCreditNote); + + // Process field mappings + await this.ingestService.processFieldMappings( + creditNote.field_mappings, + existingCreditNote.id_acc_credit_note, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingCreditNote.id_acc_credit_note, + remote_data[i], + ); + } + + return creditNoteResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/accounting/creditnote/types/index.ts b/packages/api/src/accounting/creditnote/types/index.ts index 7fe41bc14..36021a4a9 100644 --- a/packages/api/src/accounting/creditnote/types/index.ts +++ b/packages/api/src/accounting/creditnote/types/index.ts @@ -5,6 +5,7 @@ import { } from './model.unified'; import { OriginalCreditNoteOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface ICreditNoteService { addCreditNote( @@ -12,10 +13,7 @@ export interface ICreditNoteService { linkedUserId: string, ): Promise>; - syncCreditNotes( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface ICreditNoteMapper { @@ -34,5 +32,7 @@ export interface ICreditNoteMapper { slug: string; remote_id: string; }[], - ): Promise; + ): Promise< + UnifiedAccountingCreditnoteOutput | UnifiedAccountingCreditnoteOutput[] + >; } diff --git a/packages/api/src/accounting/creditnote/types/model.unified.ts b/packages/api/src/accounting/creditnote/types/model.unified.ts index f40205bfa..12a03f82b 100644 --- a/packages/api/src/accounting/creditnote/types/model.unified.ts +++ b/packages/api/src/accounting/creditnote/types/model.unified.ts @@ -1,3 +1,239 @@ -export class UnifiedAccountingCreditnoteInput {} +import { CurrencyCode } from '@@core/utils/types'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsNumber, + IsDateString, + IsArray, +} from 'class-validator'; -export class UnifiedAccountingCreditnoteOutput extends UnifiedAccountingCreditnoteInput {} +export class UnifiedAccountingCreditnoteInput { + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The date of the credit note transaction', + }) + @IsDateString() + @IsOptional() + transaction_date?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Issued', + nullable: true, + description: 'The status of the credit note', + }) + @IsString() + @IsOptional() + status?: string; + + @ApiPropertyOptional({ + type: String, + example: 'CN-001', + nullable: true, + description: 'The number of the credit note', + }) + @IsString() + @IsOptional() + number?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated contact', + }) + @IsUUID() + @IsOptional() + contact_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company', + }) + @IsUUID() + @IsOptional() + company_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '1.2', + nullable: true, + description: 'The exchange rate applied to the credit note', + }) + @IsString() + @IsOptional() + exchange_rate?: string; + + @ApiPropertyOptional({ + type: Number, + example: 10000, + nullable: true, + description: 'The total amount of the credit note', + }) + @IsNumber() + @IsOptional() + total_amount?: number; + + @ApiPropertyOptional({ + type: Number, + example: 5000, + nullable: true, + description: 'The remaining credit on the credit note', + }) + @IsNumber() + @IsOptional() + remaining_credit?: number; + + @ApiPropertyOptional({ + type: [String], + example: ['801f9ede-c698-4e66-a7fc-48d19eebaa4f'], + nullable: true, + description: + 'The UUIDs of the tracking categories associated with the credit note', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tracking_categories?: string[]; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + enum: CurrencyCode, + nullable: true, + description: 'The currency of the credit note', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: [String], + example: ['PAYMENT-001', 'PAYMENT-002'], + nullable: true, + description: 'The payments associated with the credit note', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + payments?: string[]; + + @ApiPropertyOptional({ + type: [String], + example: ['APPLIED-001', 'APPLIED-002'], + nullable: true, + description: 'The applied payments associated with the credit note', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + applied_payments?: string[]; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated accounting period', + }) + @IsUUID() + @IsOptional() + accounting_period_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingCreditnoteOutput extends UnifiedAccountingCreditnoteInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the credit note record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'creditnote_1234', + nullable: true, + description: + 'The remote ID of the credit note in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the credit note in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: + 'The date when the credit note was created in the remote system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: + 'The date when the credit note was last updated in the remote system', + }) + @IsDateString() + @IsOptional() + remote_updated_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the credit note record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the credit note record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} diff --git a/packages/api/src/accounting/expense/expense.controller.ts b/packages/api/src/accounting/expense/expense.controller.ts index 6844334ff..10f5c80d2 100644 --- a/packages/api/src/accounting/expense/expense.controller.ts +++ b/packages/api/src/accounting/expense/expense.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -34,7 +36,6 @@ import { ApiPostCustomResponse, } from '@@core/utils/dtos/openapi.respone.dto'; - @ApiTags('accounting/expenses') @Controller('accounting/expenses') export class ExpenseController { @@ -58,6 +59,7 @@ export class ExpenseController { }) @ApiPaginatedResponse(UnifiedAccountingExpenseOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getExpenses( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/accounting/expense/services/expense.service.ts b/packages/api/src/accounting/expense/services/expense.service.ts index 4ee3d01a6..60433414f 100644 --- a/packages/api/src/accounting/expense/services/expense.service.ts +++ b/packages/api/src/accounting/expense/services/expense.service.ts @@ -1,20 +1,15 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; import { UnifiedAccountingExpenseInput, UnifiedAccountingExpenseOutput, } from '../types/model.unified'; - -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; -import { OriginalExpenseOutput } from '@@core/utils/types/original/original.accounting'; - -import { IExpenseService } from '../types'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class ExpenseService { @@ -36,18 +31,172 @@ export class ExpenseService { linkedUserId: string, remote_data?: boolean, ): Promise { - return; + try { + const service = this.serviceRegistry.getService(integrationId); + const resp = await service.addExpense(unifiedExpenseData, linkedUserId); + + const savedExpense = await this.prisma.acc_expenses.create({ + data: { + id_acc_expense: uuidv4(), + ...unifiedExpenseData, + total_amount: unifiedExpenseData.total_amount + ? Number(unifiedExpenseData.total_amount) + : null, + sub_total: unifiedExpenseData.sub_total + ? Number(unifiedExpenseData.sub_total) + : null, + total_tax_amount: unifiedExpenseData.total_tax_amount + ? Number(unifiedExpenseData.total_tax_amount) + : null, + remote_id: resp.data.remote_id, + id_connection: connection_id, + created_at: new Date(), + modified_at: new Date(), + }, + }); + + // Save line items + if (unifiedExpenseData.line_items) { + await Promise.all( + unifiedExpenseData.line_items.map(async (lineItem) => { + await this.prisma.acc_expense_lines.create({ + data: { + id_acc_expense_line: uuidv4(), + id_acc_expense: savedExpense.id_acc_expense, + ...lineItem, + net_amount: lineItem.net_amount + ? Number(lineItem.net_amount) + : null, + created_at: new Date(), + modified_at: new Date(), + id_connection: connection_id, + }, + }); + }), + ); + } + + const result: UnifiedAccountingExpenseOutput = { + ...savedExpense, + currency: savedExpense.currency as CurrencyCode, + id: savedExpense.id_acc_expense, + total_amount: savedExpense.total_amount + ? Number(savedExpense.total_amount) + : undefined, + sub_total: savedExpense.sub_total + ? Number(savedExpense.sub_total) + : undefined, + total_tax_amount: savedExpense.total_tax_amount + ? Number(savedExpense.total_tax_amount) + : undefined, + line_items: unifiedExpenseData.line_items, + }; + + if (remote_data) { + result.remote_data = resp.data; + } + + return result; + } catch (error) { + throw error; + } } async getExpense( - id_expenseing_expense: string, + id_acc_expense: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const expense = await this.prisma.acc_expenses.findUnique({ + where: { id_acc_expense: id_acc_expense }, + }); + + if (!expense) { + throw new Error(`Expense with ID ${id_acc_expense} not found.`); + } + + const lineItems = await this.prisma.acc_expense_lines.findMany({ + where: { id_acc_expense: id_acc_expense }, + }); + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: expense.id_acc_expense }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedExpense: UnifiedAccountingExpenseOutput = { + id: expense.id_acc_expense, + transaction_date: expense.transaction_date, + total_amount: expense.total_amount + ? Number(expense.total_amount) + : undefined, + sub_total: expense.sub_total ? Number(expense.sub_total) : undefined, + total_tax_amount: expense.total_tax_amount + ? Number(expense.total_tax_amount) + : undefined, + currency: expense.currency as CurrencyCode, + exchange_rate: expense.exchange_rate, + memo: expense.memo, + account_id: expense.id_acc_account, + contact_id: expense.id_acc_contact, + company_info_id: expense.id_acc_company_info, + tracking_categories: expense.tracking_categories, + field_mappings: field_mappings, + remote_id: expense.remote_id, + remote_created_at: expense.remote_created_at, + created_at: expense.created_at, + modified_at: expense.modified_at, + line_items: lineItems.map((item) => ({ + id: item.id_acc_expense_line, + net_amount: item.net_amount ? Number(item.net_amount) : undefined, + currency: item.currency as CurrencyCode, + description: item.description, + exchange_rate: item.exchange_rate, + remote_id: item.remote_id, + created_at: item.created_at, + modified_at: item.modified_at, + })), + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: expense.id_acc_expense }, + }); + unifiedExpense.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.expense.pull', + method: 'GET', + url: '/accounting/expense', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedExpense; + } catch (error) { + throw error; + } } async getExpenses( @@ -58,7 +207,113 @@ export class ExpenseService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingExpenseOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const expenses = await this.prisma.acc_expenses.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_expense: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = expenses.length > limit; + if (hasNextPage) expenses.pop(); + + const unifiedExpenses = await Promise.all( + expenses.map(async (expense) => { + const lineItems = await this.prisma.acc_expense_lines.findMany({ + where: { id_acc_expense: expense.id_acc_expense }, + }); + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: expense.id_acc_expense }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedExpense: UnifiedAccountingExpenseOutput = { + id: expense.id_acc_expense, + transaction_date: expense.transaction_date, + total_amount: expense.total_amount + ? Number(expense.total_amount) + : undefined, + sub_total: expense.sub_total + ? Number(expense.sub_total) + : undefined, + total_tax_amount: expense.total_tax_amount + ? Number(expense.total_tax_amount) + : undefined, + currency: expense.currency as CurrencyCode, + exchange_rate: expense.exchange_rate, + memo: expense.memo, + account_id: expense.id_acc_account, + contact_id: expense.id_acc_contact, + company_info_id: expense.id_acc_company_info, + tracking_categories: expense.tracking_categories, + field_mappings: field_mappings, + remote_id: expense.remote_id, + remote_created_at: expense.remote_created_at, + created_at: expense.created_at, + modified_at: expense.modified_at, + line_items: lineItems.map((item) => ({ + id: item.id_acc_expense_line, + net_amount: item.net_amount ? Number(item.net_amount) : undefined, + currency: item.currency as CurrencyCode, + description: item.description, + exchange_rate: item.exchange_rate, + remote_id: item.remote_id, + created_at: item.created_at, + modified_at: item.modified_at, + })), + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: expense.id_acc_expense }, + }); + unifiedExpense.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedExpense; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.expense.pull', + method: 'GET', + url: '/accounting/expenses', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedExpenses, + next_cursor: hasNextPage + ? expenses[expenses.length - 1].id_acc_expense + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/expense/sync/sync.service.ts b/packages/api/src/accounting/expense/sync/sync.service.ts index b0d257ede..b302d6eb4 100644 --- a/packages/api/src/accounting/expense/sync/sync.service.ts +++ b/packages/api/src/accounting/expense/sync/sync.service.ts @@ -1,16 +1,24 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; - +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { OriginalExpenseOutput } from '@@core/utils/types/original/original.accounting'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_expenses as AccExpense } from '@prisma/client'; import { v4 as uuidv4 } from 'uuid'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { UnifiedAccountingExpenseOutput } from '../types/model.unified'; import { IExpenseService } from '../types'; -import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { + LineItem, + UnifiedAccountingExpenseOutput, +} from '../types/model.unified'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -20,24 +28,211 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'expense', this); } async onModuleInit() { - // Initialization logic + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting expenses...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IExpenseService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingExpenseOutput, + OriginalExpenseOutput, + IExpenseService + >(integrationId, linkedUserId, 'accounting', 'expense', service, []); + } catch (error) { + throw error; + } } - saveToDb( + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + expenses: UnifiedAccountingExpenseOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); + ): Promise { + try { + const expenseResults: AccExpense[] = []; + + for (let i = 0; i < expenses.length; i++) { + const expense = expenses[i]; + const originId = expense.remote_id; + + let existingExpense = await this.prisma.acc_expenses.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const expenseData = { + transaction_date: expense.transaction_date + ? new Date(expense.transaction_date) + : null, + total_amount: expense.total_amount + ? Number(expense.total_amount) + : null, + sub_total: expense.sub_total ? Number(expense.sub_total) : null, + total_tax_amount: expense.total_tax_amount + ? Number(expense.total_tax_amount) + : null, + currency: expense.currency as CurrencyCode, + exchange_rate: expense.exchange_rate, + memo: expense.memo, + id_acc_account: expense.account_id, + id_acc_contact: expense.contact_id, + id_acc_company_info: expense.company_info_id, + tracking_categories: expense.tracking_categories, + remote_id: originId, + remote_created_at: expense.remote_created_at, + modified_at: new Date(), + }; + + if (existingExpense) { + existingExpense = await this.prisma.acc_expenses.update({ + where: { id_acc_expense: existingExpense.id_acc_expense }, + data: expenseData, + }); + } else { + existingExpense = await this.prisma.acc_expenses.create({ + data: { + ...expenseData, + id_acc_expense: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + expenseResults.push(existingExpense); + + // Process field mappings + await this.ingestService.processFieldMappings( + expense.field_mappings, + existingExpense.id_acc_expense, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingExpense.id_acc_expense, + remote_data[i], + ); + + // Handle line items + if (expense.line_items && expense.line_items.length > 0) { + await this.processExpenseLineItems( + existingExpense.id_acc_expense, + expense.line_items, + connection_id, + ); + } + } + + return expenseResults; + } catch (error) { + throw error; + } } - // Additional methods and logic + private async processExpenseLineItems( + expenseId: string, + lineItems: LineItem[], + connectionId: string, + ): Promise { + for (const lineItem of lineItems) { + const lineItemData = { + id_acc_expense: expenseId, + remote_id: lineItem.remote_id, + net_amount: lineItem.net_amount ? Number(lineItem.net_amount) : null, + currency: lineItem.currency as CurrencyCode, + description: lineItem.description, + exchange_rate: lineItem.exchange_rate, + modified_at: new Date(), + id_connection: connectionId, + }; + + const existingLineItem = await this.prisma.acc_expense_lines.findFirst({ + where: { + remote_id: lineItem.remote_id, + id_acc_expense: expenseId, + }, + }); + + if (existingLineItem) { + await this.prisma.acc_expense_lines.update({ + where: { + id_acc_expense_line: existingLineItem.id_acc_expense_line, + }, + data: lineItemData, + }); + } else { + await this.prisma.acc_expense_lines.create({ + data: { + ...lineItemData, + id_acc_expense_line: uuidv4(), + created_at: new Date(), + }, + }); + } + } + + // Remove any existing line items that are not in the current set + const currentRemoteIds = lineItems.map((item) => item.remote_id); + await this.prisma.acc_expense_lines.deleteMany({ + where: { + id_acc_expense: expenseId, + remote_id: { + notIn: currentRemoteIds, + }, + }, + }); + } } diff --git a/packages/api/src/accounting/expense/types/index.ts b/packages/api/src/accounting/expense/types/index.ts index a6326fe16..d4de9fe9b 100644 --- a/packages/api/src/accounting/expense/types/index.ts +++ b/packages/api/src/accounting/expense/types/index.ts @@ -1,7 +1,11 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { UnifiedAccountingExpenseInput, UnifiedAccountingExpenseOutput } from './model.unified'; +import { + UnifiedAccountingExpenseInput, + UnifiedAccountingExpenseOutput, +} from './model.unified'; import { OriginalExpenseOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IExpenseService { addExpense( @@ -9,10 +13,7 @@ export interface IExpenseService { linkedUserId: string, ): Promise>; - syncExpenses( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IExpenseMapper { diff --git a/packages/api/src/accounting/expense/types/model.unified.ts b/packages/api/src/accounting/expense/types/model.unified.ts index 52d124695..9d1537f3c 100644 --- a/packages/api/src/accounting/expense/types/model.unified.ts +++ b/packages/api/src/accounting/expense/types/model.unified.ts @@ -1,3 +1,282 @@ -export class UnifiedAccountingExpenseInput {} +import { CurrencyCode } from '@@core/utils/types'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsNumber, + IsDateString, + IsArray, +} from 'class-validator'; -export class UnifiedAccountingExpenseOutput extends UnifiedAccountingExpenseInput {} +export class LineItem { + @ApiPropertyOptional({ + type: Number, + example: 5000, + nullable: true, + description: 'The net amount of the line item in cents', + }) + @IsNumber() + @IsOptional() + net_amount?: number; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + enum: CurrencyCode, + nullable: true, + description: 'The currency of the line item', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: String, + example: 'Office supplies', + nullable: true, + description: 'Description of the line item', + }) + @IsString() + @IsOptional() + description?: string; + + @ApiPropertyOptional({ + type: String, + example: '1.0', + nullable: true, + description: 'The exchange rate for the line item', + }) + @IsString() + @IsOptional() + exchange_rate?: string; + + @ApiPropertyOptional({ + type: String, + example: 'line_item_1234', + nullable: true, + description: 'The remote ID of the line item', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + description: 'The created date of the line item', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + description: 'The last modified date of the line item', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} +export class UnifiedAccountingExpenseInput { + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The date of the expense transaction', + }) + @IsDateString() + @IsOptional() + transaction_date?: Date; + + @ApiPropertyOptional({ + type: Number, + example: 10000, + nullable: true, + description: 'The total amount of the expense', + }) + @IsNumber() + @IsOptional() + total_amount?: number; + + @ApiPropertyOptional({ + type: Number, + example: 9000, + nullable: true, + description: 'The sub-total amount of the expense (before tax)', + }) + @IsNumber() + @IsOptional() + sub_total?: number; + + @ApiPropertyOptional({ + type: Number, + example: 1000, + nullable: true, + description: 'The total tax amount of the expense', + }) + @IsNumber() + @IsOptional() + total_tax_amount?: number; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + enum: CurrencyCode, + nullable: true, + description: 'The currency of the expense', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: String, + example: '1.2', + nullable: true, + description: 'The exchange rate applied to the expense', + }) + @IsString() + @IsOptional() + exchange_rate?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Business lunch with client', + nullable: true, + description: 'A memo or description for the expense', + }) + @IsString() + @IsOptional() + memo?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated account', + }) + @IsUUID() + @IsOptional() + account_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated contact', + }) + @IsUUID() + @IsOptional() + contact_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company info', + }) + @IsUUID() + @IsOptional() + company_info_id?: string; + + @ApiPropertyOptional({ + type: [String], + example: ['801f9ede-c698-4e66-a7fc-48d19eebaa4f'], + nullable: true, + description: + 'The UUIDs of the tracking categories associated with the expense', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tracking_categories?: string[]; + + @ApiPropertyOptional({ + type: [LineItem], + description: 'The line items associated with this expense', + }) + @IsArray() + @IsOptional() + line_items?: LineItem[]; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingExpenseOutput extends UnifiedAccountingExpenseInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the expense record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'expense_1234', + nullable: true, + description: 'The remote ID of the expense in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the expense in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The date when the expense was created in the remote system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the expense record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the expense record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} diff --git a/packages/api/src/accounting/incomestatement/incomestatement.controller.ts b/packages/api/src/accounting/incomestatement/incomestatement.controller.ts index 30bc355f7..6bc0a5cb0 100644 --- a/packages/api/src/accounting/incomestatement/incomestatement.controller.ts +++ b/packages/api/src/accounting/incomestatement/incomestatement.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -28,8 +30,10 @@ import { import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { QueryDto } from '@@core/utils/dtos/query.dto'; -import { ApiGetCustomResponse, ApiPaginatedResponse } from '@@core/utils/dtos/openapi.respone.dto'; - +import { + ApiGetCustomResponse, + ApiPaginatedResponse, +} from '@@core/utils/dtos/openapi.respone.dto'; @ApiTags('accounting/incomestatements') @Controller('accounting/incomestatements') @@ -54,6 +58,7 @@ export class IncomeStatementController { }) @ApiPaginatedResponse(UnifiedAccountingIncomestatementOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getIncomeStatements( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/accounting/incomestatement/services/incomestatement.service.ts b/packages/api/src/accounting/incomestatement/services/incomestatement.service.ts index bf10f8326..ed3a4f3e6 100644 --- a/packages/api/src/accounting/incomestatement/services/incomestatement.service.ts +++ b/packages/api/src/accounting/incomestatement/services/incomestatement.service.ts @@ -2,19 +2,15 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; +import { ApiResponse, CurrencyCode } from '@@core/utils/types'; import { throwTypedError } from '@@core/utils/errors'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { UnifiedAccountingIncomestatementInput, UnifiedAccountingIncomestatementOutput, } from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; -import { OriginalIncomeStatementOutput } from '@@core/utils/types/original/original.accounting'; - -import { IIncomeStatementService } from '../types'; @Injectable() export class IncomeStatementService { @@ -29,14 +25,90 @@ export class IncomeStatementService { } async getIncomeStatement( - id_incomestatementing_incomestatement: string, + id_acc_income_statement: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const incomeStatement = + await this.prisma.acc_income_statements.findUnique({ + where: { id_acc_income_statement: id_acc_income_statement }, + }); + + if (!incomeStatement) { + throw new Error( + `Income statement with ID ${id_acc_income_statement} not found.`, + ); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: incomeStatement.id_acc_income_statement, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedIncomeStatement: UnifiedAccountingIncomestatementOutput = { + id: incomeStatement.id_acc_income_statement, + name: incomeStatement.name, + currency: incomeStatement.currency as CurrencyCode, + start_period: incomeStatement.start_period, + end_period: incomeStatement.end_period, + gross_profit: incomeStatement.gross_profit + ? Number(incomeStatement.gross_profit) + : undefined, + net_operating_income: incomeStatement.net_operating_income + ? Number(incomeStatement.net_operating_income) + : undefined, + net_income: incomeStatement.net_income + ? Number(incomeStatement.net_income) + : undefined, + field_mappings: field_mappings, + remote_id: incomeStatement.remote_id, + created_at: incomeStatement.created_at, + modified_at: incomeStatement.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: incomeStatement.id_acc_income_statement, + }, + }); + unifiedIncomeStatement.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.income_statement.pull', + method: 'GET', + url: '/accounting/income_statement', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedIncomeStatement; + } catch (error) { + throw error; + } } async getIncomeStatements( @@ -47,7 +119,102 @@ export class IncomeStatementService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingIncomestatementOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const incomeStatements = await this.prisma.acc_income_statements.findMany( + { + take: limit + 1, + cursor: cursor ? { id_acc_income_statement: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }, + ); + + const hasNextPage = incomeStatements.length > limit; + if (hasNextPage) incomeStatements.pop(); + + const unifiedIncomeStatements = await Promise.all( + incomeStatements.map(async (incomeStatement) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: incomeStatement.id_acc_income_statement, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedIncomeStatement: UnifiedAccountingIncomestatementOutput = + { + id: incomeStatement.id_acc_income_statement, + name: incomeStatement.name, + currency: incomeStatement.currency as CurrencyCode, + start_period: incomeStatement.start_period, + end_period: incomeStatement.end_period, + gross_profit: incomeStatement.gross_profit + ? Number(incomeStatement.gross_profit) + : undefined, + net_operating_income: incomeStatement.net_operating_income + ? Number(incomeStatement.net_operating_income) + : undefined, + net_income: incomeStatement.net_income + ? Number(incomeStatement.net_income) + : undefined, + field_mappings: field_mappings, + remote_id: incomeStatement.remote_id, + created_at: incomeStatement.created_at, + modified_at: incomeStatement.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: incomeStatement.id_acc_income_statement, + }, + }); + unifiedIncomeStatement.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedIncomeStatement; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.income_statement.pull', + method: 'GET', + url: '/accounting/income_statements', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedIncomeStatements, + next_cursor: hasNextPage + ? incomeStatements[incomeStatements.length - 1] + .id_acc_income_statement + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/incomestatement/sync/sync.service.ts b/packages/api/src/accounting/incomestatement/sync/sync.service.ts index 4391aa1f7..3e4415bb7 100644 --- a/packages/api/src/accounting/incomestatement/sync/sync.service.ts +++ b/packages/api/src/accounting/incomestatement/sync/sync.service.ts @@ -1,9 +1,8 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; - import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ApiResponse, CurrencyCode } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; @@ -11,6 +10,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedAccountingIncomestatementOutput } from '../types/model.unified'; import { IIncomeStatementService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_income_statements as AccIncomeStatement } from '@prisma/client'; +import { OriginalIncomeStatementOutput } from '@@core/utils/types/original/original.accounting'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -20,23 +25,156 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'income_statement', this); } async onModuleInit() { - // Initialization logic + // Initialization logic if needed } - saveToDb( + + @Cron('0 */12 * * *') // every 12 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting income statements...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IIncomeStatementService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingIncomestatementOutput, + OriginalIncomeStatementOutput, + IIncomeStatementService + >( + integrationId, + linkedUserId, + 'accounting', + 'income_statement', + service, + [], + ); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + incomeStatements: UnifiedAccountingIncomestatementOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const incomeStatementResults: AccIncomeStatement[] = []; - // Additional methods and logic + for (let i = 0; i < incomeStatements.length; i++) { + const incomeStatement = incomeStatements[i]; + const originId = incomeStatement.remote_id; + + let existingIncomeStatement = + await this.prisma.acc_income_statements.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const incomeStatementData = { + name: incomeStatement.name, + currency: incomeStatement.currency as CurrencyCode, + start_period: incomeStatement.start_period, + end_period: incomeStatement.end_period, + gross_profit: incomeStatement.gross_profit + ? Number(incomeStatement.gross_profit) + : null, + net_operating_income: incomeStatement.net_operating_income + ? Number(incomeStatement.net_operating_income) + : null, + net_income: incomeStatement.net_income + ? Number(incomeStatement.net_income) + : null, + remote_id: originId, + modified_at: new Date(), + }; + + if (existingIncomeStatement) { + existingIncomeStatement = + await this.prisma.acc_income_statements.update({ + where: { + id_acc_income_statement: + existingIncomeStatement.id_acc_income_statement, + }, + data: incomeStatementData, + }); + } else { + existingIncomeStatement = + await this.prisma.acc_income_statements.create({ + data: { + ...incomeStatementData, + id_acc_income_statement: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + incomeStatementResults.push(existingIncomeStatement); + + // Process field mappings + await this.ingestService.processFieldMappings( + incomeStatement.field_mappings, + existingIncomeStatement.id_acc_income_statement, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingIncomeStatement.id_acc_income_statement, + remote_data[i], + ); + } + + return incomeStatementResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/accounting/incomestatement/types/index.ts b/packages/api/src/accounting/incomestatement/types/index.ts index 3d0fef96e..e22d33cc8 100644 --- a/packages/api/src/accounting/incomestatement/types/index.ts +++ b/packages/api/src/accounting/incomestatement/types/index.ts @@ -5,6 +5,7 @@ import { } from './model.unified'; import { OriginalIncomeStatementOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IIncomeStatementService { addIncomeStatement( @@ -12,10 +13,7 @@ export interface IIncomeStatementService { linkedUserId: string, ): Promise>; - syncIncomeStatements( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IIncomeStatementMapper { @@ -34,5 +32,8 @@ export interface IIncomeStatementMapper { slug: string; remote_id: string; }[], - ): Promise; + ): Promise< + | UnifiedAccountingIncomestatementOutput + | UnifiedAccountingIncomestatementOutput[] + >; } diff --git a/packages/api/src/accounting/incomestatement/types/model.unified.ts b/packages/api/src/accounting/incomestatement/types/model.unified.ts index 1baee39a2..18123d79d 100644 --- a/packages/api/src/accounting/incomestatement/types/model.unified.ts +++ b/packages/api/src/accounting/incomestatement/types/model.unified.ts @@ -1,3 +1,152 @@ -export class UnifiedAccountingIncomestatementInput {} +import { CurrencyCode } from '@@core/utils/types'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsNumber, + IsDateString, +} from 'class-validator'; -export class UnifiedAccountingIncomestatementOutput extends UnifiedAccountingIncomestatementInput {} +export class UnifiedAccountingIncomestatementInput { + @ApiPropertyOptional({ + type: String, + example: 'Q2 2024 Income Statement', + nullable: true, + description: 'The name of the income statement', + }) + @IsString() + @IsOptional() + name?: string; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + enum: CurrencyCode, + nullable: true, + description: 'The currency used in the income statement', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: Date, + example: '2024-04-01T00:00:00Z', + nullable: true, + description: 'The start date of the period covered by the income statement', + }) + @IsDateString() + @IsOptional() + start_period?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-30T23:59:59Z', + nullable: true, + description: 'The end date of the period covered by the income statement', + }) + @IsDateString() + @IsOptional() + end_period?: Date; + + @ApiPropertyOptional({ + type: Number, + example: 1000000, + nullable: true, + description: 'The gross profit for the period', + }) + @IsNumber() + @IsOptional() + gross_profit?: number; + + @ApiPropertyOptional({ + type: Number, + example: 800000, + nullable: true, + description: 'The net operating income for the period', + }) + @IsNumber() + @IsOptional() + net_operating_income?: number; + + @ApiPropertyOptional({ + type: Number, + example: 750000, + nullable: true, + description: 'The net income for the period', + }) + @IsNumber() + @IsOptional() + net_income?: number; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingIncomestatementOutput extends UnifiedAccountingIncomestatementInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the income statement record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'incomestatement_1234', + nullable: true, + description: + 'The remote ID of the income statement in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the income statement in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the income statement record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the income statement record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} diff --git a/packages/api/src/accounting/invoice/invoice.controller.ts b/packages/api/src/accounting/invoice/invoice.controller.ts index 327e7ec4d..409227556 100644 --- a/packages/api/src/accounting/invoice/invoice.controller.ts +++ b/packages/api/src/accounting/invoice/invoice.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -34,7 +36,6 @@ import { ApiPostCustomResponse, } from '@@core/utils/dtos/openapi.respone.dto'; - @ApiTags('accounting/invoices') @Controller('accounting/invoices') export class InvoiceController { @@ -58,6 +59,7 @@ export class InvoiceController { }) @ApiPaginatedResponse(UnifiedAccountingInvoiceOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getInvoices( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/accounting/invoice/services/invoice.service.ts b/packages/api/src/accounting/invoice/services/invoice.service.ts index 565c493c6..ac846a2d5 100644 --- a/packages/api/src/accounting/invoice/services/invoice.service.ts +++ b/packages/api/src/accounting/invoice/services/invoice.service.ts @@ -1,20 +1,15 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; import { UnifiedAccountingInvoiceInput, UnifiedAccountingInvoiceOutput, } from '../types/model.unified'; - -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; -import { OriginalInvoiceOutput } from '@@core/utils/types/original/original.accounting'; - -import { IInvoiceService } from '../types'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class InvoiceService { @@ -36,18 +31,203 @@ export class InvoiceService { linkedUserId: string, remote_data?: boolean, ): Promise { - return; + try { + const service = this.serviceRegistry.getService(integrationId); + const resp = await service.addInvoice(unifiedInvoiceData, linkedUserId); + + const savedInvoice = await this.prisma.acc_invoices.create({ + data: { + id_acc_invoice: uuidv4(), + ...unifiedInvoiceData, + total_discount: unifiedInvoiceData.total_discount + ? Number(unifiedInvoiceData.total_discount) + : null, + sub_total: unifiedInvoiceData.sub_total + ? Number(unifiedInvoiceData.sub_total) + : null, + total_tax_amount: unifiedInvoiceData.total_tax_amount + ? Number(unifiedInvoiceData.total_tax_amount) + : null, + total_amount: unifiedInvoiceData.total_amount + ? Number(unifiedInvoiceData.total_amount) + : null, + balance: unifiedInvoiceData.balance + ? Number(unifiedInvoiceData.balance) + : null, + remote_id: resp.data.remote_id, + id_connection: connection_id, + created_at: new Date(), + modified_at: new Date(), + }, + }); + + // Save line items + if (unifiedInvoiceData.line_items) { + await Promise.all( + unifiedInvoiceData.line_items.map(async (lineItem) => { + await this.prisma.acc_invoices_line_items.create({ + data: { + id_acc_invoices_line_item: uuidv4(), + id_acc_invoice: savedInvoice.id_acc_invoice, + id_acc_item: uuidv4(), + ...lineItem, + unit_price: lineItem.unit_price + ? Number(lineItem.unit_price) + : null, + quantity: lineItem.quantity ? Number(lineItem.quantity) : null, + total_amount: lineItem.total_amount + ? Number(lineItem.total_amount) + : null, + created_at: new Date(), + modified_at: new Date(), + id_connection: connection_id, + }, + }); + }), + ); + } + + const result: UnifiedAccountingInvoiceOutput = { + ...savedInvoice, + currency: savedInvoice.currency as CurrencyCode, + id: savedInvoice.id_acc_invoice, + total_discount: savedInvoice.total_discount + ? Number(savedInvoice.total_discount) + : undefined, + sub_total: savedInvoice.sub_total + ? Number(savedInvoice.sub_total) + : undefined, + total_tax_amount: savedInvoice.total_tax_amount + ? Number(savedInvoice.total_tax_amount) + : undefined, + total_amount: savedInvoice.total_amount + ? Number(savedInvoice.total_amount) + : undefined, + balance: savedInvoice.balance + ? Number(savedInvoice.balance) + : undefined, + line_items: unifiedInvoiceData.line_items, + }; + + if (remote_data) { + result.remote_data = resp.data; + } + + return result; + } catch (error) { + throw error; + } } async getInvoice( - id_invoiceing_invoice: string, + id_acc_invoice: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const invoice = await this.prisma.acc_invoices.findUnique({ + where: { id_acc_invoice: id_acc_invoice }, + }); + + if (!invoice) { + throw new Error(`Invoice with ID ${id_acc_invoice} not found.`); + } + + const lineItems = await this.prisma.acc_invoices_line_items.findMany({ + where: { id_acc_invoice: id_acc_invoice }, + }); + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: invoice.id_acc_invoice }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedInvoice: UnifiedAccountingInvoiceOutput = { + id: invoice.id_acc_invoice, + type: invoice.type, + number: invoice.number, + issue_date: invoice.issue_date, + due_date: invoice.due_date, + paid_on_date: invoice.paid_on_date, + memo: invoice.memo, + currency: invoice.currency as CurrencyCode, + exchange_rate: invoice.exchange_rate, + total_discount: invoice.total_discount + ? Number(invoice.total_discount) + : undefined, + sub_total: invoice.sub_total ? Number(invoice.sub_total) : undefined, + status: invoice.status, + total_tax_amount: invoice.total_tax_amount + ? Number(invoice.total_tax_amount) + : undefined, + total_amount: invoice.total_amount + ? Number(invoice.total_amount) + : undefined, + balance: invoice.balance ? Number(invoice.balance) : undefined, + contact_id: invoice.id_acc_contact, + accounting_period_id: invoice.id_acc_accounting_period, + tracking_categories: invoice.tracking_categories, + field_mappings: field_mappings, + remote_id: invoice.remote_id, + remote_updated_at: invoice.remote_updated_at, + created_at: invoice.created_at, + modified_at: invoice.modified_at, + line_items: lineItems.map((item) => ({ + id: item.id_acc_invoices_line_item, + description: item.description, + unit_price: item.unit_price ? Number(item.unit_price) : undefined, + quantity: item.quantity ? Number(item.quantity) : undefined, + total_amount: item.total_amount + ? Number(item.total_amount) + : undefined, + currency: item.currency as CurrencyCode, + exchange_rate: item.exchange_rate, + id_acc_item: item.id_acc_item, + acc_tracking_categories: item.acc_tracking_categories, + remote_id: item.remote_id, + created_at: item.created_at, + modified_at: item.modified_at, + })), + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: invoice.id_acc_invoice }, + }); + unifiedInvoice.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.invoice.pull', + method: 'GET', + url: '/accounting/invoice', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedInvoice; + } catch (error) { + throw error; + } } async getInvoices( @@ -58,7 +238,127 @@ export class InvoiceService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingInvoiceOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const invoices = await this.prisma.acc_invoices.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_invoice: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = invoices.length > limit; + if (hasNextPage) invoices.pop(); + + const unifiedInvoices = await Promise.all( + invoices.map(async (invoice) => { + const lineItems = await this.prisma.acc_invoices_line_items.findMany({ + where: { id_acc_invoice: invoice.id_acc_invoice }, + }); + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: invoice.id_acc_invoice }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedInvoice: UnifiedAccountingInvoiceOutput = { + id: invoice.id_acc_invoice, + type: invoice.type, + number: invoice.number, + issue_date: invoice.issue_date, + due_date: invoice.due_date, + paid_on_date: invoice.paid_on_date, + memo: invoice.memo, + currency: invoice.currency as CurrencyCode, + exchange_rate: invoice.exchange_rate, + total_discount: invoice.total_discount + ? Number(invoice.total_discount) + : undefined, + sub_total: invoice.sub_total + ? Number(invoice.sub_total) + : undefined, + status: invoice.status, + total_tax_amount: invoice.total_tax_amount + ? Number(invoice.total_tax_amount) + : undefined, + total_amount: invoice.total_amount + ? Number(invoice.total_amount) + : undefined, + balance: invoice.balance ? Number(invoice.balance) : undefined, + contact_id: invoice.id_acc_contact, + accounting_period_id: invoice.id_acc_accounting_period, + tracking_categories: invoice.tracking_categories, + field_mappings: field_mappings, + remote_id: invoice.remote_id, + remote_updated_at: invoice.remote_updated_at, + created_at: invoice.created_at, + modified_at: invoice.modified_at, + line_items: lineItems.map((item) => ({ + id: item.id_acc_invoices_line_item, + description: item.description, + unit_price: item.unit_price ? Number(item.unit_price) : undefined, + quantity: item.quantity ? Number(item.quantity) : undefined, + total_amount: item.total_amount + ? Number(item.total_amount) + : undefined, + currency: item.currency as CurrencyCode, + exchange_rate: item.exchange_rate, + id_acc_item: item.id_acc_item, + acc_tracking_categories: item.acc_tracking_categories, + remote_id: item.remote_id, + created_at: item.created_at, + modified_at: item.modified_at, + })), + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: invoice.id_acc_invoice }, + }); + unifiedInvoice.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedInvoice; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.invoice.pull', + method: 'GET', + url: '/accounting/invoices', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedInvoices, + next_cursor: hasNextPage + ? invoices[invoices.length - 1].id_acc_invoice + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/invoice/sync/sync.service.ts b/packages/api/src/accounting/invoice/sync/sync.service.ts index f4c245a60..c3fa05443 100644 --- a/packages/api/src/accounting/invoice/sync/sync.service.ts +++ b/packages/api/src/accounting/invoice/sync/sync.service.ts @@ -1,16 +1,24 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; - +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { OriginalInvoiceOutput } from '@@core/utils/types/original/original.accounting'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_invoices as AccInvoice } from '@prisma/client'; import { v4 as uuidv4 } from 'uuid'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { UnifiedAccountingInvoiceOutput } from '../types/model.unified'; import { IInvoiceService } from '../types'; -import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { + LineItem, + UnifiedAccountingInvoiceOutput, +} from '../types/model.unified'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -20,22 +28,225 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'invoice', this); } async onModuleInit() { - // Initialization logic + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting invoices...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IInvoiceService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingInvoiceOutput, + OriginalInvoiceOutput, + IInvoiceService + >(integrationId, linkedUserId, 'accounting', 'invoice', service, []); + } catch (error) { + throw error; + } } - saveToDb( + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + invoices: UnifiedAccountingInvoiceOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); + ): Promise { + try { + const invoiceResults: AccInvoice[] = []; + + for (let i = 0; i < invoices.length; i++) { + const invoice = invoices[i]; + const originId = invoice.remote_id; + + let existingInvoice = await this.prisma.acc_invoices.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const invoiceData = { + type: invoice.type, + number: invoice.number, + issue_date: invoice.issue_date, + due_date: invoice.due_date, + paid_on_date: invoice.paid_on_date, + memo: invoice.memo, + currency: invoice.currency as CurrencyCode, + exchange_rate: invoice.exchange_rate, + total_discount: invoice.total_discount + ? Number(invoice.total_discount) + : null, + sub_total: invoice.sub_total ? Number(invoice.sub_total) : null, + status: invoice.status, + total_tax_amount: invoice.total_tax_amount + ? Number(invoice.total_tax_amount) + : null, + total_amount: invoice.total_amount + ? Number(invoice.total_amount) + : null, + balance: invoice.balance ? Number(invoice.balance) : null, + remote_updated_at: invoice.remote_updated_at, + remote_id: originId, + id_acc_contact: invoice.contact_id, + id_acc_accounting_period: invoice.accounting_period_id, + tracking_categories: invoice.tracking_categories, + modified_at: new Date(), + }; + + if (existingInvoice) { + existingInvoice = await this.prisma.acc_invoices.update({ + where: { id_acc_invoice: existingInvoice.id_acc_invoice }, + data: invoiceData, + }); + } else { + existingInvoice = await this.prisma.acc_invoices.create({ + data: { + ...invoiceData, + id_acc_invoice: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + invoiceResults.push(existingInvoice); + + // Process field mappings + await this.ingestService.processFieldMappings( + invoice.field_mappings, + existingInvoice.id_acc_invoice, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingInvoice.id_acc_invoice, + remote_data[i], + ); + + // Handle line items + if (invoice.line_items && invoice.line_items.length > 0) { + await this.processInvoiceLineItems( + existingInvoice.id_acc_invoice, + invoice.line_items, + connection_id, + ); + } + } + + return invoiceResults; + } catch (error) { + throw error; + } + } + + private async processInvoiceLineItems( + invoiceId: string, + lineItems: LineItem[], + connectionId: string, + ): Promise { + for (const lineItem of lineItems) { + const lineItemData = { + description: lineItem.description, + unit_price: lineItem.unit_price ? Number(lineItem.unit_price) : null, + quantity: lineItem.quantity ? Number(lineItem.quantity) : null, + total_amount: lineItem.total_amount + ? Number(lineItem.total_amount) + : null, + currency: lineItem.currency as CurrencyCode, + exchange_rate: lineItem.exchange_rate, + id_acc_invoice: invoiceId, + id_acc_item: lineItem.item_id, + acc_tracking_categories: lineItem.tracking_categories, + remote_id: lineItem.remote_id, + modified_at: new Date(), + id_connection: connectionId, + }; + + const existingLineItem = + await this.prisma.acc_invoices_line_items.findFirst({ + where: { + remote_id: lineItem.remote_id, + id_acc_invoice: invoiceId, + }, + }); + + if (existingLineItem) { + await this.prisma.acc_invoices_line_items.update({ + where: { + id_acc_invoices_line_item: + existingLineItem.id_acc_invoices_line_item, + }, + data: lineItemData, + }); + } else { + await this.prisma.acc_invoices_line_items.create({ + data: { + ...lineItemData, + id_acc_invoices_line_item: uuidv4(), + created_at: new Date(), + }, + }); + } + } + + // Remove any existing line items that are not in the current set + const currentRemoteIds = lineItems.map((item) => item.remote_id); + await this.prisma.acc_invoices_line_items.deleteMany({ + where: { + id_acc_invoice: invoiceId, + remote_id: { + notIn: currentRemoteIds, + }, + }, + }); } - // Additional methods and logic } diff --git a/packages/api/src/accounting/invoice/types/index.ts b/packages/api/src/accounting/invoice/types/index.ts index dd800275f..cddb0c418 100644 --- a/packages/api/src/accounting/invoice/types/index.ts +++ b/packages/api/src/accounting/invoice/types/index.ts @@ -1,7 +1,11 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { UnifiedAccountingInvoiceInput, UnifiedAccountingInvoiceOutput } from './model.unified'; +import { + UnifiedAccountingInvoiceInput, + UnifiedAccountingInvoiceOutput, +} from './model.unified'; import { OriginalInvoiceOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IInvoiceService { addInvoice( @@ -9,10 +13,7 @@ export interface IInvoiceService { linkedUserId: string, ): Promise>; - syncInvoices( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IInvoiceMapper { diff --git a/packages/api/src/accounting/invoice/types/model.unified.ts b/packages/api/src/accounting/invoice/types/model.unified.ts index 9ec1789bf..ce0625651 100644 --- a/packages/api/src/accounting/invoice/types/model.unified.ts +++ b/packages/api/src/accounting/invoice/types/model.unified.ts @@ -1,3 +1,389 @@ -export class UnifiedAccountingInvoiceInput {} +import { CurrencyCode } from '@@core/utils/types'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsNumber, + IsDateString, + IsArray, +} from 'class-validator'; -export class UnifiedAccountingInvoiceOutput extends UnifiedAccountingInvoiceInput {} +export class LineItem { + @ApiPropertyOptional({ + type: String, + example: 'Product description', + nullable: true, + description: 'Description of the line item', + }) + @IsString() + @IsOptional() + description?: string; + + @ApiPropertyOptional({ + type: Number, + example: 1000, + nullable: true, + description: 'The unit price of the item in cents', + }) + @IsNumber() + @IsOptional() + unit_price?: number; + + @ApiPropertyOptional({ + type: Number, + example: 2, + nullable: true, + description: 'The quantity of the item', + }) + @IsNumber() + @IsOptional() + quantity?: number; + + @ApiPropertyOptional({ + type: Number, + example: 2000, + nullable: true, + description: 'The total amount for the line item in cents', + }) + @IsNumber() + @IsOptional() + total_amount?: number; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + enum: CurrencyCode, + nullable: true, + description: 'The currency of the line item', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: String, + example: '1.0', + nullable: true, + description: 'The exchange rate for the line item', + }) + @IsString() + @IsOptional() + exchange_rate?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated item', + }) + @IsUUID() + @IsOptional() + item_id?: string; + + @ApiPropertyOptional({ + type: [String], + example: ['801f9ede-c698-4e66-a7fc-48d19eebaa4f'], + nullable: true, + description: + 'The UUIDs of the tracking categories associated with the line item', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tracking_categories?: string[]; + + @ApiPropertyOptional({ + type: String, + example: 'line_item_1234', + nullable: true, + description: 'The remote ID of the line item', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + description: 'The created date of the line item', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + description: 'The last modified date of the line item', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} + +export class UnifiedAccountingInvoiceInput { + @ApiPropertyOptional({ + type: String, + example: 'Sales', + nullable: true, + description: 'The type of the invoice', + }) + @IsString() + @IsOptional() + type?: string; + + @ApiPropertyOptional({ + type: String, + example: 'INV-001', + nullable: true, + description: 'The invoice number', + }) + @IsString() + @IsOptional() + number?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The date the invoice was issued', + }) + @IsDateString() + @IsOptional() + issue_date?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-07-15T12:00:00Z', + nullable: true, + description: 'The due date of the invoice', + }) + @IsDateString() + @IsOptional() + due_date?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-07-10T12:00:00Z', + nullable: true, + description: 'The date the invoice was paid', + }) + @IsDateString() + @IsOptional() + paid_on_date?: Date; + + @ApiPropertyOptional({ + type: String, + example: 'Payment for services rendered', + nullable: true, + description: 'A memo or note on the invoice', + }) + @IsString() + @IsOptional() + memo?: string; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + enum: CurrencyCode, + nullable: true, + description: 'The currency of the invoice', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: String, + example: '1.2', + nullable: true, + description: 'The exchange rate applied to the invoice', + }) + @IsString() + @IsOptional() + exchange_rate?: string; + + @ApiPropertyOptional({ + type: Number, + example: 1000, + nullable: true, + description: 'The total discount applied to the invoice', + }) + @IsNumber() + @IsOptional() + total_discount?: number; + + @ApiPropertyOptional({ + type: Number, + example: 10000, + nullable: true, + description: 'The subtotal of the invoice', + }) + @IsNumber() + @IsOptional() + sub_total?: number; + + @ApiPropertyOptional({ + type: String, + example: 'Paid', + nullable: true, + description: 'The status of the invoice', + }) + @IsString() + @IsOptional() + status?: string; + + @ApiPropertyOptional({ + type: Number, + example: 1000, + nullable: true, + description: 'The total tax amount on the invoice', + }) + @IsNumber() + @IsOptional() + total_tax_amount?: number; + + @ApiPropertyOptional({ + type: Number, + example: 11000, + nullable: true, + description: 'The total amount of the invoice', + }) + @IsNumber() + @IsOptional() + total_amount?: number; + + @ApiPropertyOptional({ + type: Number, + example: 0, + nullable: true, + description: 'The remaining balance on the invoice', + }) + @IsNumber() + @IsOptional() + balance?: number; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated contact', + }) + @IsUUID() + @IsOptional() + contact_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated accounting period', + }) + @IsUUID() + @IsOptional() + accounting_period_id?: string; // todo + + @ApiPropertyOptional({ + type: [String], + example: [ + '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + ], + nullable: true, + description: + 'The UUIDs of the tracking categories associated with the invoice', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tracking_categories?: string[]; + + @ApiPropertyOptional({ + type: [LineItem], + description: 'The line items associated with this invoice', + }) + @IsArray() + @IsOptional() + line_items?: LineItem[]; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingInvoiceOutput extends UnifiedAccountingInvoiceInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the invoice record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'invoice_1234', + nullable: true, + description: 'The remote ID of the invoice in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the invoice in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: + 'The date when the invoice was last updated in the remote system', + }) + @IsDateString() + @IsOptional() + remote_updated_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the invoice record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the invoice record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} diff --git a/packages/api/src/accounting/item/item.controller.ts b/packages/api/src/accounting/item/item.controller.ts index dc4b16165..528109d83 100644 --- a/packages/api/src/accounting/item/item.controller.ts +++ b/packages/api/src/accounting/item/item.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -28,8 +30,10 @@ import { import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { QueryDto } from '@@core/utils/dtos/query.dto'; -import { ApiGetCustomResponse, ApiPaginatedResponse } from '@@core/utils/dtos/openapi.respone.dto'; - +import { + ApiGetCustomResponse, + ApiPaginatedResponse, +} from '@@core/utils/dtos/openapi.respone.dto'; @ApiTags('accounting/items') @Controller('accounting/items') @@ -54,6 +58,7 @@ export class ItemController { }) @ApiPaginatedResponse(UnifiedAccountingItemOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getItems( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/accounting/item/services/item.service.ts b/packages/api/src/accounting/item/services/item.service.ts index f9e00132a..e53a8be21 100644 --- a/packages/api/src/accounting/item/services/item.service.ts +++ b/packages/api/src/accounting/item/services/item.service.ts @@ -5,13 +5,12 @@ import { v4 as uuidv4 } from 'uuid'; import { ApiResponse } from '@@core/utils/types'; import { throwTypedError } from '@@core/utils/errors'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { UnifiedAccountingItemInput, UnifiedAccountingItemOutput } from '../types/model.unified'; - +import { + UnifiedAccountingItemInput, + UnifiedAccountingItemOutput, +} from '../types/model.unified'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; -import { OriginalItemOutput } from '@@core/utils/types/original/original.accounting'; - -import { IItemService } from '../types'; @Injectable() export class ItemService { @@ -26,16 +25,81 @@ export class ItemService { } async getItem( - id_iteming_item: string, + id_acc_item: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; - } + try { + const item = await this.prisma.acc_items.findUnique({ + where: { id_acc_item: id_acc_item }, + }); + + if (!item) { + throw new Error(`Item with ID ${id_acc_item} not found.`); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: item.id_acc_item }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedItem: UnifiedAccountingItemOutput = { + id: item.id_acc_item, + name: item.name, + status: item.status, + unit_price: item.unit_price ? Number(item.unit_price) : undefined, + purchase_price: item.purchase_price + ? Number(item.purchase_price) + : undefined, + sales_account: item.sales_account, + purchase_account: item.purchase_account, + company_info_id: item.id_acc_company_info, + field_mappings: field_mappings, + remote_id: item.remote_id, + remote_updated_at: item.remote_updated_at, + created_at: item.created_at, + modified_at: item.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: item.id_acc_item }, + }); + unifiedItem.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.item.pull', + method: 'GET', + url: '/accounting/item', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + return unifiedItem; + } catch (error) { + throw error; + } + } async getItems( connectionId: string, projectId: string, @@ -44,7 +108,89 @@ export class ItemService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingItemOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const items = await this.prisma.acc_items.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_item: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = items.length > limit; + if (hasNextPage) items.pop(); + + const unifiedItems = await Promise.all( + items.map(async (item) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: item.id_acc_item }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedItem: UnifiedAccountingItemOutput = { + id: item.id_acc_item, + name: item.name, + status: item.status, + unit_price: item.unit_price ? Number(item.unit_price) : undefined, + purchase_price: item.purchase_price + ? Number(item.purchase_price) + : undefined, + sales_account: item.sales_account, + purchase_account: item.purchase_account, + company_info_id: item.id_acc_company_info, + field_mappings: field_mappings, + remote_id: item.remote_id, + remote_updated_at: item.remote_updated_at, + created_at: item.created_at, + modified_at: item.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: item.id_acc_item }, + }); + unifiedItem.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedItem; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.item.pull', + method: 'GET', + url: '/accounting/items', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedItems, + next_cursor: hasNextPage ? items[items.length - 1].id_acc_item : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/item/sync/sync.service.ts b/packages/api/src/accounting/item/sync/sync.service.ts index 3e5573cf6..a6df008da 100644 --- a/packages/api/src/accounting/item/sync/sync.service.ts +++ b/packages/api/src/accounting/item/sync/sync.service.ts @@ -1,7 +1,6 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; - import { Cron } from '@nestjs/schedule'; import { ApiResponse } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; @@ -11,6 +10,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedAccountingItemOutput } from '../types/model.unified'; import { IItemService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_items as AccItem } from '@prisma/client'; +import { OriginalItemOutput } from '@@core/utils/types/original/original.accounting'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -20,23 +25,140 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'item', this); } async onModuleInit() { - // Initialization logic + // Initialization logic if needed } - saveToDb( + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting items...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IItemService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingItemOutput, + OriginalItemOutput, + IItemService + >(integrationId, linkedUserId, 'accounting', 'item', service, []); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + items: UnifiedAccountingItemOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const itemResults: AccItem[] = []; - // Additional methods and logic + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const originId = item.remote_id; + + let existingItem = await this.prisma.acc_items.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const itemData = { + name: item.name, + status: item.status, + unit_price: item.unit_price ? Number(item.unit_price) : null, + purchase_price: item.purchase_price + ? Number(item.purchase_price) + : null, + remote_updated_at: item.remote_updated_at, + remote_id: originId, + sales_account: item.sales_account, + purchase_account: item.purchase_account, + id_acc_company_info: item.company_info_id, + modified_at: new Date(), + }; + + if (existingItem) { + existingItem = await this.prisma.acc_items.update({ + where: { id_acc_item: existingItem.id_acc_item }, + data: itemData, + }); + } else { + existingItem = await this.prisma.acc_items.create({ + data: { + ...itemData, + id_acc_item: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + itemResults.push(existingItem); + + // Process field mappings + await this.ingestService.processFieldMappings( + item.field_mappings, + existingItem.id_acc_item, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingItem.id_acc_item, + remote_data[i], + ); + } + + return itemResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/accounting/item/types/index.ts b/packages/api/src/accounting/item/types/index.ts index dd67857d8..a9f79296d 100644 --- a/packages/api/src/accounting/item/types/index.ts +++ b/packages/api/src/accounting/item/types/index.ts @@ -1,7 +1,11 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { UnifiedAccountingItemInput, UnifiedAccountingItemOutput } from './model.unified'; +import { + UnifiedAccountingItemInput, + UnifiedAccountingItemOutput, +} from './model.unified'; import { OriginalItemOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IItemService { addItem( @@ -9,10 +13,7 @@ export interface IItemService { linkedUserId: string, ): Promise>; - syncItems( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IItemMapper { diff --git a/packages/api/src/accounting/item/types/model.unified.ts b/packages/api/src/accounting/item/types/model.unified.ts index 3200cb8bd..013d547b8 100644 --- a/packages/api/src/accounting/item/types/model.unified.ts +++ b/packages/api/src/accounting/item/types/model.unified.ts @@ -1,3 +1,158 @@ -export class UnifiedAccountingItemInput {} +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsNumber, + IsDateString, +} from 'class-validator'; -export class UnifiedAccountingItemOutput extends UnifiedAccountingItemInput {} +export class UnifiedAccountingItemInput { + @ApiPropertyOptional({ + type: String, + example: 'Product A', + nullable: true, + description: 'The name of the accounting item', + }) + @IsString() + @IsOptional() + name?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Active', + nullable: true, + description: 'The status of the accounting item', + }) + @IsString() + @IsOptional() + status?: string; + + @ApiPropertyOptional({ + type: Number, + example: 1000, + nullable: true, + description: 'The unit price of the item in cents', + }) + @IsNumber() + @IsOptional() + unit_price?: number; + + @ApiPropertyOptional({ + type: Number, + example: 800, + nullable: true, + description: 'The purchase price of the item in cents', + }) + @IsNumber() + @IsOptional() + purchase_price?: number; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated sales account', + }) + @IsUUID() + @IsOptional() + sales_account?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated purchase account', + }) + @IsUUID() + @IsOptional() + purchase_account?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company info', + }) + @IsUUID() + @IsOptional() + company_info_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingItemOutput extends UnifiedAccountingItemInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the accounting item record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'item_1234', + nullable: true, + description: 'The remote ID of the item in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The date when the item was last updated in the remote system', + }) + @IsDateString() + @IsOptional() + remote_updated_at?: Date; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: 'The remote data of the item in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the accounting item record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the accounting item record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} diff --git a/packages/api/src/accounting/journalentry/journalentry.controller.ts b/packages/api/src/accounting/journalentry/journalentry.controller.ts index f5a28880e..30aa881ae 100644 --- a/packages/api/src/accounting/journalentry/journalentry.controller.ts +++ b/packages/api/src/accounting/journalentry/journalentry.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -34,7 +36,6 @@ import { ApiPostCustomResponse, } from '@@core/utils/dtos/openapi.respone.dto'; - @ApiTags('accounting/journalentries') @Controller('accounting/journalentries') export class JournalEntryController { @@ -58,6 +59,7 @@ export class JournalEntryController { }) @ApiPaginatedResponse(UnifiedAccountingJournalentryOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getJournalEntrys( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/accounting/journalentry/services/journalentry.service.ts b/packages/api/src/accounting/journalentry/services/journalentry.service.ts index 43f636714..dd2562544 100644 --- a/packages/api/src/accounting/journalentry/services/journalentry.service.ts +++ b/packages/api/src/accounting/journalentry/services/journalentry.service.ts @@ -1,20 +1,15 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; import { UnifiedAccountingJournalentryInput, UnifiedAccountingJournalentryOutput, } from '../types/model.unified'; - -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; -import { OriginalJournalEntryOutput } from '@@core/utils/types/original/original.accounting'; - -import { IJournalEntryService } from '../types'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class JournalEntryService { @@ -36,18 +31,158 @@ export class JournalEntryService { linkedUserId: string, remote_data?: boolean, ): Promise { - return; + try { + const service = this.serviceRegistry.getService(integrationId); + const resp = await service.addJournalEntry( + unifiedJournalEntryData, + linkedUserId, + ); + + const savedJournalEntry = await this.prisma.acc_journal_entries.create({ + data: { + id_acc_journal_entry: uuidv4(), + ...unifiedJournalEntryData, + remote_id: resp.data.remote_id, + id_connection: connection_id, + created_at: new Date(), + modified_at: new Date(), + }, + }); + + // Save line items + if (unifiedJournalEntryData.line_items) { + await Promise.all( + unifiedJournalEntryData.line_items.map(async (lineItem) => { + await this.prisma.acc_journal_entries_lines.create({ + data: { + id_acc_journal_entries_line: uuidv4(), + id_acc_journal_entry: savedJournalEntry.id_acc_journal_entry, + ...lineItem, + net_amount: lineItem.net_amount + ? Number(lineItem.net_amount) + : null, + created_at: new Date(), + modified_at: new Date(), + }, + }); + }), + ); + } + + const result: UnifiedAccountingJournalentryOutput = { + ...savedJournalEntry, + currency: savedJournalEntry.currency as CurrencyCode, + id: savedJournalEntry.id_acc_journal_entry, + line_items: unifiedJournalEntryData.line_items, + }; + + if (remote_data) { + result.remote_data = resp.data; + } + + return result; + } catch (error) { + throw error; + } } async getJournalEntry( - id_journalentrying_journalentry: string, + id_acc_journal_entry: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const journalEntry = await this.prisma.acc_journal_entries.findUnique({ + where: { id_acc_journal_entry: id_acc_journal_entry }, + }); + + if (!journalEntry) { + throw new Error( + `Journal entry with ID ${id_acc_journal_entry} not found.`, + ); + } + + const lineItems = await this.prisma.acc_journal_entries_lines.findMany({ + where: { id_acc_journal_entry: id_acc_journal_entry }, + }); + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: journalEntry.id_acc_journal_entry }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedJournalEntry: UnifiedAccountingJournalentryOutput = { + id: journalEntry.id_acc_journal_entry, + transaction_date: journalEntry.transaction_date, + payments: journalEntry.payments, + applied_payments: journalEntry.applied_payments, + memo: journalEntry.memo, + currency: journalEntry.currency as CurrencyCode, + exchange_rate: journalEntry.exchange_rate, + id_acc_company_info: journalEntry.id_acc_company_info, + journal_number: journalEntry.journal_number, + tracking_categories: journalEntry.tracking_categories, + id_acc_accounting_period: journalEntry.id_acc_accounting_period, + posting_status: journalEntry.posting_status, + field_mappings: field_mappings, + remote_id: journalEntry.remote_id, + remote_created_at: journalEntry.remote_created_at, + remote_modiified_at: journalEntry.remote_modiified_at, + created_at: journalEntry.created_at, + modified_at: journalEntry.modified_at, + line_items: lineItems.map((item) => ({ + id: item.id_acc_journal_entries_line, + net_amount: item.net_amount ? Number(item.net_amount) : undefined, + tracking_categories: item.tracking_categories, + currency: item.currency as CurrencyCode, + description: item.description, + company: item.company, + contact: item.contact, + exchange_rate: item.exchange_rate, + remote_id: item.remote_id, + created_at: item.created_at, + modified_at: item.modified_at, + })), + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: journalEntry.id_acc_journal_entry }, + }); + unifiedJournalEntry.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.journal_entry.pull', + method: 'GET', + url: '/accounting/journal_entry', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedJournalEntry; + } catch (error) { + throw error; + } } async getJournalEntrys( @@ -58,7 +193,114 @@ export class JournalEntryService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingJournalentryOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const journalEntries = await this.prisma.acc_journal_entries.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_journal_entry: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = journalEntries.length > limit; + if (hasNextPage) journalEntries.pop(); + + const unifiedJournalEntries = await Promise.all( + journalEntries.map(async (journalEntry) => { + const lineItems = + await this.prisma.acc_journal_entries_lines.findMany({ + where: { + id_acc_journal_entry: journalEntry.id_acc_journal_entry, + }, + }); + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: journalEntry.id_acc_journal_entry }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedJournalEntry: UnifiedAccountingJournalentryOutput = { + id: journalEntry.id_acc_journal_entry, + transaction_date: journalEntry.transaction_date, + payments: journalEntry.payments, + applied_payments: journalEntry.applied_payments, + memo: journalEntry.memo, + currency: journalEntry.currency as CurrencyCode, + exchange_rate: journalEntry.exchange_rate, + id_acc_company_info: journalEntry.id_acc_company_info, + journal_number: journalEntry.journal_number, + tracking_categories: journalEntry.tracking_categories, + id_acc_accounting_period: journalEntry.id_acc_accounting_period, + posting_status: journalEntry.posting_status, + field_mappings: field_mappings, + remote_id: journalEntry.remote_id, + remote_created_at: journalEntry.remote_created_at, + remote_modiified_at: journalEntry.remote_modiified_at, + created_at: journalEntry.created_at, + modified_at: journalEntry.modified_at, + line_items: lineItems.map((item) => ({ + id: item.id_acc_journal_entries_line, + net_amount: item.net_amount ? Number(item.net_amount) : undefined, + tracking_categories: item.tracking_categories, + currency: item.currency as CurrencyCode, + description: item.description, + company: item.company, + contact: item.contact, + exchange_rate: item.exchange_rate, + remote_id: item.remote_id, + created_at: item.created_at, + modified_at: item.modified_at, + })), + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: journalEntry.id_acc_journal_entry }, + }); + unifiedJournalEntry.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedJournalEntry; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.journal_entry.pull', + method: 'GET', + url: '/accounting/journal_entries', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedJournalEntries, + next_cursor: hasNextPage + ? journalEntries[journalEntries.length - 1].id_acc_journal_entry + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/journalentry/sync/sync.service.ts b/packages/api/src/accounting/journalentry/sync/sync.service.ts index 9cfd866c3..8c5da919b 100644 --- a/packages/api/src/accounting/journalentry/sync/sync.service.ts +++ b/packages/api/src/accounting/journalentry/sync/sync.service.ts @@ -1,16 +1,24 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; - +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { OriginalJournalEntryOutput } from '@@core/utils/types/original/original.accounting'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_journal_entries as AccJournalEntry } from '@prisma/client'; import { v4 as uuidv4 } from 'uuid'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { UnifiedAccountingJournalentryOutput } from '../types/model.unified'; import { IJournalEntryService } from '../types'; -import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { + LineItem, + UnifiedAccountingJournalentryOutput, +} from '../types/model.unified'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -20,23 +28,218 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'journal_entry', this); } async onModuleInit() { - // Initialization logic + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting journal entries...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IJournalEntryService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingJournalentryOutput, + OriginalJournalEntryOutput, + IJournalEntryService + >( + integrationId, + linkedUserId, + 'accounting', + 'journal_entry', + service, + [], + ); + } catch (error) { + throw error; + } } - saveToDb( + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + journalEntries: UnifiedAccountingJournalentryOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); + ): Promise { + try { + const journalEntryResults: AccJournalEntry[] = []; + + for (let i = 0; i < journalEntries.length; i++) { + const journalEntry = journalEntries[i]; + const originId = journalEntry.remote_id; + + let existingJournalEntry = + await this.prisma.acc_journal_entries.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const journalEntryData = { + transaction_date: journalEntry.transaction_date, + payments: journalEntry.payments, + applied_payments: journalEntry.applied_payments, + memo: journalEntry.memo, + currency: journalEntry.currency as CurrencyCode, + exchange_rate: journalEntry.exchange_rate, + id_acc_company_info: journalEntry.id_acc_company_info, + journal_number: journalEntry.journal_number, + tracking_categories: journalEntry.tracking_categories, + id_acc_accounting_period: journalEntry.id_acc_accounting_period, + posting_status: journalEntry.posting_status, + remote_created_at: journalEntry.remote_created_at, + remote_modiified_at: journalEntry.remote_modiified_at, + remote_id: originId, + modified_at: new Date(), + }; + + if (existingJournalEntry) { + existingJournalEntry = await this.prisma.acc_journal_entries.update({ + where: { + id_acc_journal_entry: existingJournalEntry.id_acc_journal_entry, + }, + data: journalEntryData, + }); + } else { + existingJournalEntry = await this.prisma.acc_journal_entries.create({ + data: { + ...journalEntryData, + id_acc_journal_entry: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + journalEntryResults.push(existingJournalEntry); + + // Process field mappings + await this.ingestService.processFieldMappings( + journalEntry.field_mappings, + existingJournalEntry.id_acc_journal_entry, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingJournalEntry.id_acc_journal_entry, + remote_data[i], + ); + + // Handle line items + if (journalEntry.line_items && journalEntry.line_items.length > 0) { + await this.processJournalEntryLineItems( + existingJournalEntry.id_acc_journal_entry, + journalEntry.line_items, + ); + } + } + + return journalEntryResults; + } catch (error) { + throw error; + } } - // Additional methods and logic + private async processJournalEntryLineItems( + journalEntryId: string, + lineItems: LineItem[], + ): Promise { + for (const lineItem of lineItems) { + const lineItemData = { + net_amount: lineItem.net_amount ? Number(lineItem.net_amount) : null, + tracking_categories: lineItem.tracking_categories, + currency: lineItem.currency as CurrencyCode, + description: lineItem.description, + company: lineItem.company, + contact: lineItem.contact, + exchange_rate: lineItem.exchange_rate, + remote_id: lineItem.remote_id, + modified_at: new Date(), + id_acc_journal_entry: journalEntryId, + }; + + const existingLineItem = + await this.prisma.acc_journal_entries_lines.findFirst({ + where: { + remote_id: lineItem.remote_id, + id_acc_journal_entry: journalEntryId, + }, + }); + + if (existingLineItem) { + await this.prisma.acc_journal_entries_lines.update({ + where: { + id_acc_journal_entries_line: + existingLineItem.id_acc_journal_entries_line, + }, + data: lineItemData, + }); + } else { + await this.prisma.acc_journal_entries_lines.create({ + data: { + ...lineItemData, + id_acc_journal_entries_line: uuidv4(), + created_at: new Date(), + }, + }); + } + } + + // Remove any existing line items that are not in the current set + const currentRemoteIds = lineItems.map((item) => item.remote_id); + await this.prisma.acc_journal_entries_lines.deleteMany({ + where: { + id_acc_journal_entry: journalEntryId, + remote_id: { + notIn: currentRemoteIds, + }, + }, + }); + } } diff --git a/packages/api/src/accounting/journalentry/types/index.ts b/packages/api/src/accounting/journalentry/types/index.ts index 2c2171a75..c6779f2d1 100644 --- a/packages/api/src/accounting/journalentry/types/index.ts +++ b/packages/api/src/accounting/journalentry/types/index.ts @@ -5,6 +5,7 @@ import { } from './model.unified'; import { OriginalJournalEntryOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IJournalEntryService { addJournalEntry( @@ -12,10 +13,7 @@ export interface IJournalEntryService { linkedUserId: string, ): Promise>; - syncJournalEntrys( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IJournalEntryMapper { @@ -34,5 +32,7 @@ export interface IJournalEntryMapper { slug: string; remote_id: string; }[], - ): Promise; + ): Promise< + UnifiedAccountingJournalentryOutput | UnifiedAccountingJournalentryOutput[] + >; } diff --git a/packages/api/src/accounting/journalentry/types/model.unified.ts b/packages/api/src/accounting/journalentry/types/model.unified.ts index 2d9ece7c6..36a1bdb06 100644 --- a/packages/api/src/accounting/journalentry/types/model.unified.ts +++ b/packages/api/src/accounting/journalentry/types/model.unified.ts @@ -1,3 +1,328 @@ -export class UnifiedAccountingJournalentryInput {} +import { CurrencyCode } from '@@core/utils/types'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsString, + IsDateString, + IsArray, + IsOptional, + IsNumber, +} from 'class-validator'; -export class UnifiedAccountingJournalentryOutput extends UnifiedAccountingJournalentryInput {} +export class LineItem { + @ApiPropertyOptional({ + type: Number, + example: 10000, + nullable: true, + description: 'The net amount of the line item in cents', + }) + @IsNumber() + @IsOptional() + net_amount?: number; + + @ApiPropertyOptional({ + type: [String], + example: ['801f9ede-c698-4e66-a7fc-48d19eebaa4f'], + nullable: true, + description: + 'The UUIDs of the tracking categories associated with the line item', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tracking_categories?: string[]; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + enum: CurrencyCode, + nullable: true, + description: 'The currency of the line item', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: String, + example: 'Office supplies expense', + nullable: true, + description: 'Description of the line item', + }) + @IsString() + @IsOptional() + description?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company', + }) + @IsUUID() + @IsOptional() + company?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated contact', + }) + @IsUUID() + @IsOptional() + contact?: string; + + @ApiPropertyOptional({ + type: String, + example: '1.2', + nullable: true, + description: 'The exchange rate applied to the line item', + }) + @IsString() + @IsOptional() + exchange_rate?: string; + + @ApiPropertyOptional({ + type: String, + example: 'line_item_1234', + nullable: true, + description: 'The remote ID of the line item', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + description: 'The created date of the line item', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + description: 'The last modified date of the line item', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} + +export class UnifiedAccountingJournalentryInput { + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The date of the transaction', + }) + @IsDateString() + @IsOptional() + transaction_date?: Date; + + @ApiPropertyOptional({ + type: [String], + example: ['payment1', 'payment2'], + nullable: true, + description: 'The payments associated with the journal entry', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + payments?: string[]; + + @ApiPropertyOptional({ + type: [String], + example: ['appliedPayment1', 'appliedPayment2'], + nullable: true, + description: 'The applied payments for the journal entry', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + applied_payments?: string[]; + + @ApiPropertyOptional({ + type: String, + example: 'Monthly expense journal entry', + nullable: true, + description: 'A memo or note for the journal entry', + }) + @IsString() + @IsOptional() + memo?: string; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + enum: CurrencyCode, + nullable: true, + description: 'The currency of the journal entry', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: String, + example: '1.2', + nullable: true, + description: 'The exchange rate applied to the journal entry', + }) + @IsString() + @IsOptional() + exchange_rate?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: false, + description: 'The UUID of the associated company info', + }) + @IsUUID() + id_acc_company_info: string; + + @ApiPropertyOptional({ + type: String, + example: 'JE-001', + nullable: true, + description: 'The journal number', + }) + @IsString() + @IsOptional() + journal_number?: string; + + @ApiPropertyOptional({ + type: [String], + example: ['801f9ede-c698-4e66-a7fc-48d19eebaa4f'], + nullable: true, + description: + 'The UUIDs of the tracking categories associated with the journal entry', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tracking_categories?: string[]; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated accounting period', + }) + @IsUUID() + @IsOptional() + id_acc_accounting_period?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Posted', + nullable: true, + description: 'The posting status of the journal entry', + }) + @IsString() + @IsOptional() + posting_status?: string; + + @ApiPropertyOptional({ + type: [LineItem], + description: 'The line items associated with this journal entry', + }) + @IsArray() + @IsOptional() + line_items?: LineItem[]; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingJournalentryOutput extends UnifiedAccountingJournalentryInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the journal entry record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'journal_entry_1234', + nullable: false, + description: + 'The remote ID of the journal entry in the context of the 3rd Party', + }) + @IsString() + remote_id: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: + 'The date when the journal entry was created in the remote system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: + 'The date when the journal entry was last modified in the remote system', + }) + @IsDateString() + @IsOptional() + remote_modiified_at?: Date; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the journal entry in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the journal entry record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the journal entry record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} diff --git a/packages/api/src/accounting/payment/payment.controller.ts b/packages/api/src/accounting/payment/payment.controller.ts index b13303421..f7ca0658f 100644 --- a/packages/api/src/accounting/payment/payment.controller.ts +++ b/packages/api/src/accounting/payment/payment.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -57,6 +59,7 @@ export class PaymentController { }) @ApiPaginatedResponse(UnifiedAccountingPaymentOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getPayments( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/accounting/payment/services/payment.service.ts b/packages/api/src/accounting/payment/services/payment.service.ts index 10e07f30c..56f04b382 100644 --- a/packages/api/src/accounting/payment/services/payment.service.ts +++ b/packages/api/src/accounting/payment/services/payment.service.ts @@ -1,20 +1,15 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; import { UnifiedAccountingPaymentInput, UnifiedAccountingPaymentOutput, } from '../types/model.unified'; - -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; -import { OriginalPaymentOutput } from '@@core/utils/types/original/original.accounting'; - -import { IPaymentService } from '../types'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class PaymentService { @@ -36,18 +31,160 @@ export class PaymentService { linkedUserId: string, remote_data?: boolean, ): Promise { - return; + try { + const service = this.serviceRegistry.getService(integrationId); + const resp = await service.addPayment(unifiedPaymentData, linkedUserId); + + const savedPayment = await this.prisma.acc_payments.create({ + data: { + id_acc_payment: uuidv4(), + ...unifiedPaymentData, + total_amount: unifiedPaymentData.total_amount + ? Number(unifiedPaymentData.total_amount) + : null, + remote_id: resp.data.remote_id, + id_connection: connection_id, + created_at: new Date(), + modified_at: new Date(), + }, + }); + + // Save line items + if (unifiedPaymentData.line_items) { + await Promise.all( + unifiedPaymentData.line_items.map(async (lineItem) => { + await this.prisma.acc_payments_line_items.create({ + data: { + acc_payments_line_item: uuidv4(), + id_acc_payment: savedPayment.id_acc_payment, + ...lineItem, + applied_amount: lineItem.applied_amount + ? Number(lineItem.applied_amount) + : null, + created_at: new Date(), + modified_at: new Date(), + id_connection: connection_id, + }, + }); + }), + ); + } + + const result: UnifiedAccountingPaymentOutput = { + ...savedPayment, + currency: savedPayment.currency as CurrencyCode, + id: savedPayment.id_acc_payment, + total_amount: savedPayment.total_amount + ? Number(savedPayment.total_amount) + : undefined, + line_items: unifiedPaymentData.line_items, + }; + + if (remote_data) { + result.remote_data = resp.data; + } + + return result; + } catch (error) { + throw error; + } } async getPayment( - id_paymenting_payment: string, + id_acc_payment: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const payment = await this.prisma.acc_payments.findUnique({ + where: { id_acc_payment: id_acc_payment }, + }); + + if (!payment) { + throw new Error(`Payment with ID ${id_acc_payment} not found.`); + } + + const lineItems = await this.prisma.acc_payments_line_items.findMany({ + where: { id_acc_payment: id_acc_payment }, + }); + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: payment.id_acc_payment }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedPayment: UnifiedAccountingPaymentOutput = { + id: payment.id_acc_payment, + invoice_id: payment.id_acc_invoice, + transaction_date: payment.transaction_date, + contact_id: payment.id_acc_contact, + account_id: payment.id_acc_account, + currency: payment.currency as CurrencyCode, + exchange_rate: payment.exchange_rate, + total_amount: payment.total_amount + ? Number(payment.total_amount) + : undefined, + type: payment.type, + company_info_id: payment.id_acc_company_info, + accounting_period_id: payment.id_acc_accounting_period, + tracking_categories: payment.tracking_categories, + field_mappings: field_mappings, + remote_id: payment.remote_id, + remote_updated_at: payment.remote_updated_at, + created_at: payment.created_at, + modified_at: payment.modified_at, + line_items: lineItems.map((item) => ({ + id: item.acc_payments_line_item, + applied_amount: item.applied_amount + ? Number(item.applied_amount) + : undefined, + applied_date: item.applied_date, + related_object_id: item.related_object_id, + related_object_type: item.related_object_type, + remote_id: item.remote_id, + created_at: item.created_at, + modified_at: item.modified_at, + })), + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: payment.id_acc_payment }, + }); + unifiedPayment.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.payment.pull', + method: 'GET', + url: '/accounting/payment', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedPayment; + } catch (error) { + throw error; + } } async getPayments( @@ -58,7 +195,111 @@ export class PaymentService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingPaymentOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const payments = await this.prisma.acc_payments.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_payment: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = payments.length > limit; + if (hasNextPage) payments.pop(); + + const unifiedPayments = await Promise.all( + payments.map(async (payment) => { + const lineItems = await this.prisma.acc_payments_line_items.findMany({ + where: { id_acc_payment: payment.id_acc_payment }, + }); + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: payment.id_acc_payment }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedPayment: UnifiedAccountingPaymentOutput = { + id: payment.id_acc_payment, + invoice_id: payment.id_acc_invoice, + transaction_date: payment.transaction_date, + contact_id: payment.id_acc_contact, + account_id: payment.id_acc_account, + currency: payment.currency as CurrencyCode, + exchange_rate: payment.exchange_rate, + total_amount: payment.total_amount + ? Number(payment.total_amount) + : undefined, + type: payment.type, + company_info_id: payment.id_acc_company_info, + accounting_period_id: payment.id_acc_accounting_period, + tracking_categories: payment.tracking_categories, + field_mappings: field_mappings, + remote_id: payment.remote_id, + remote_updated_at: payment.remote_updated_at, + created_at: payment.created_at, + modified_at: payment.modified_at, + line_items: lineItems.map((item) => ({ + id: item.acc_payments_line_item, + applied_amount: item.applied_amount + ? Number(item.applied_amount) + : undefined, + applied_date: item.applied_date, + related_object_id: item.related_object_id, + related_object_type: item.related_object_type, + remote_id: item.remote_id, + created_at: item.created_at, + modified_at: item.modified_at, + })), + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: payment.id_acc_payment }, + }); + unifiedPayment.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedPayment; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.payment.pull', + method: 'GET', + url: '/accounting/payments', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedPayments, + next_cursor: hasNextPage + ? payments[payments.length - 1].id_acc_payment + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/payment/sync/sync.service.ts b/packages/api/src/accounting/payment/sync/sync.service.ts index 1a34b3669..cf1da5205 100644 --- a/packages/api/src/accounting/payment/sync/sync.service.ts +++ b/packages/api/src/accounting/payment/sync/sync.service.ts @@ -1,16 +1,24 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; - +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { OriginalPaymentOutput } from '@@core/utils/types/original/original.accounting'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_payments as AccPayment } from '@prisma/client'; import { v4 as uuidv4 } from 'uuid'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { UnifiedAccountingPaymentOutput } from '../types/model.unified'; import { IPaymentService } from '../types'; -import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { + LineItem, + UnifiedAccountingPaymentOutput, +} from '../types/model.unified'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -20,23 +28,210 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'payment', this); } async onModuleInit() { - // Initialization logic + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting payments...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IPaymentService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingPaymentOutput, + OriginalPaymentOutput, + IPaymentService + >(integrationId, linkedUserId, 'accounting', 'payment', service, []); + } catch (error) { + throw error; + } } - saveToDb( + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + payments: UnifiedAccountingPaymentOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); + ): Promise { + try { + const paymentResults: AccPayment[] = []; + + for (let i = 0; i < payments.length; i++) { + const payment = payments[i]; + const originId = payment.remote_id; + + let existingPayment = await this.prisma.acc_payments.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const paymentData = { + id_acc_invoice: payment.invoice_id, + transaction_date: payment.transaction_date, + id_acc_contact: payment.contact_id, + id_acc_account: payment.account_id, + currency: payment.currency as CurrencyCode, + exchange_rate: payment.exchange_rate, + total_amount: payment.total_amount + ? Number(payment.total_amount) + : null, + type: payment.type, + remote_updated_at: payment.remote_updated_at, + id_acc_company_info: payment.company_info_id, + id_acc_accounting_period: payment.accounting_period_id, + tracking_categories: payment.tracking_categories, + remote_id: originId, + modified_at: new Date(), + }; + + if (existingPayment) { + existingPayment = await this.prisma.acc_payments.update({ + where: { id_acc_payment: existingPayment.id_acc_payment }, + data: paymentData, + }); + } else { + existingPayment = await this.prisma.acc_payments.create({ + data: { + ...paymentData, + id_acc_payment: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + paymentResults.push(existingPayment); + + // Process field mappings + await this.ingestService.processFieldMappings( + payment.field_mappings, + existingPayment.id_acc_payment, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingPayment.id_acc_payment, + remote_data[i], + ); + + // Handle line items + if (payment.line_items && payment.line_items.length > 0) { + await this.processPaymentLineItems( + existingPayment.id_acc_payment, + payment.line_items, + connection_id, + ); + } + } + + return paymentResults; + } catch (error) { + throw error; + } } - // Additional methods and logic + private async processPaymentLineItems( + paymentId: string, + lineItems: LineItem[], + connectionId: string, + ): Promise { + for (const lineItem of lineItems) { + const lineItemData = { + id_acc_payment: paymentId, + applied_amount: lineItem.applied_amount + ? Number(lineItem.applied_amount) + : null, + applied_date: lineItem.applied_date, + related_object_id: lineItem.related_object_id, + related_object_type: lineItem.related_object_type, + remote_id: lineItem.remote_id, + modified_at: new Date(), + id_connection: connectionId, + }; + + const existingLineItem = + await this.prisma.acc_payments_line_items.findFirst({ + where: { + remote_id: lineItem.remote_id, + id_acc_payment: paymentId, + }, + }); + + if (existingLineItem) { + await this.prisma.acc_payments_line_items.update({ + where: { + acc_payments_line_item: existingLineItem.acc_payments_line_item, + }, + data: lineItemData, + }); + } else { + await this.prisma.acc_payments_line_items.create({ + data: { + ...lineItemData, + acc_payments_line_item: uuidv4(), + created_at: new Date(), + }, + }); + } + } + + // Remove any existing line items that are not in the current set + const currentRemoteIds = lineItems.map((item) => item.remote_id); + await this.prisma.acc_payments_line_items.deleteMany({ + where: { + id_acc_payment: paymentId, + remote_id: { + notIn: currentRemoteIds, + }, + }, + }); + } } diff --git a/packages/api/src/accounting/payment/types/index.ts b/packages/api/src/accounting/payment/types/index.ts index 4d1b0ef89..c76e30747 100644 --- a/packages/api/src/accounting/payment/types/index.ts +++ b/packages/api/src/accounting/payment/types/index.ts @@ -1,7 +1,11 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { UnifiedAccountingPaymentInput, UnifiedAccountingPaymentOutput } from './model.unified'; +import { + UnifiedAccountingPaymentInput, + UnifiedAccountingPaymentOutput, +} from './model.unified'; import { OriginalPaymentOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IPaymentService { addPayment( @@ -9,10 +13,7 @@ export interface IPaymentService { linkedUserId: string, ): Promise>; - syncPayments( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IPaymentMapper { diff --git a/packages/api/src/accounting/payment/types/model.unified.ts b/packages/api/src/accounting/payment/types/model.unified.ts index bded5d6ad..aa85513dd 100644 --- a/packages/api/src/accounting/payment/types/model.unified.ts +++ b/packages/api/src/accounting/payment/types/model.unified.ts @@ -1,3 +1,283 @@ -export class UnifiedAccountingPaymentInput {} +import { CurrencyCode } from '@@core/utils/types'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsNumber, + IsDateString, + IsArray, +} from 'class-validator'; -export class UnifiedAccountingPaymentOutput extends UnifiedAccountingPaymentInput {} +export class LineItem { + @ApiPropertyOptional({ + type: Number, + example: 5000, + nullable: true, + description: 'The applied amount in cents', + }) + @IsNumber() + @IsOptional() + applied_amount?: number; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The date when the amount was applied', + }) + @IsDateString() + @IsOptional() + applied_date?: Date; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the related object (e.g., invoice)', + }) + @IsUUID() + @IsOptional() + related_object_id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'invoice', + nullable: true, + description: 'The type of the related object', + }) + @IsString() + @IsOptional() + related_object_type?: string; + + @ApiPropertyOptional({ + type: String, + example: 'line_item_1234', + nullable: true, + description: 'The remote ID of the line item', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + description: 'The created date of the line item', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + description: 'The last modified date of the line item', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} + +export class UnifiedAccountingPaymentInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated invoice', + }) + @IsUUID() + @IsOptional() + invoice_id?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The date of the transaction', + }) + @IsDateString() + @IsOptional() + transaction_date?: Date; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated contact', + }) + @IsUUID() + @IsOptional() + contact_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated account', + }) + @IsUUID() + @IsOptional() + account_id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + enum: CurrencyCode, + nullable: true, + description: 'The currency of the payment', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: String, + example: '1.2', + nullable: true, + description: 'The exchange rate applied to the payment', + }) + @IsString() + @IsOptional() + exchange_rate?: string; + + @ApiPropertyOptional({ + type: Number, + example: 10000, + nullable: true, + description: 'The total amount of the payment in cents', + }) + @IsNumber() + @IsOptional() + total_amount?: number; + + @ApiPropertyOptional({ + type: String, + example: 'Credit Card', + nullable: true, + description: 'The type of payment', + }) + @IsString() + @IsOptional() + type?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company info', + }) + @IsUUID() + @IsOptional() + company_info_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated accounting period', + }) + @IsUUID() + @IsOptional() + accounting_period_id?: string; + + @ApiPropertyOptional({ + type: [String], + example: ['801f9ede-c698-4e66-a7fc-48d19eebaa4f'], + nullable: true, + description: + 'The UUIDs of the tracking categories associated with the payment', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tracking_categories?: string[]; + + @ApiPropertyOptional({ + type: [LineItem], + description: 'The line items associated with this payment', + }) + @IsArray() + @IsOptional() + line_items?: LineItem[]; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingPaymentOutput extends UnifiedAccountingPaymentInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the payment record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'payment_1234', + nullable: true, + description: 'The remote ID of the payment in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: + 'The date when the payment was last updated in the remote system', + }) + @IsDateString() + @IsOptional() + remote_updated_at?: Date; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the payment in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the payment record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the payment record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} diff --git a/packages/api/src/accounting/phonenumber/phonenumber.controller.ts b/packages/api/src/accounting/phonenumber/phonenumber.controller.ts index d0f61d3a5..d974bec27 100644 --- a/packages/api/src/accounting/phonenumber/phonenumber.controller.ts +++ b/packages/api/src/accounting/phonenumber/phonenumber.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -28,8 +30,10 @@ import { import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { QueryDto } from '@@core/utils/dtos/query.dto'; -import { ApiGetCustomResponse, ApiPaginatedResponse } from '@@core/utils/dtos/openapi.respone.dto'; - +import { + ApiGetCustomResponse, + ApiPaginatedResponse, +} from '@@core/utils/dtos/openapi.respone.dto'; @ApiTags('accounting/phonenumbers') @Controller('accounting/phonenumbers') @@ -52,6 +56,7 @@ export class PhoneNumberController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @ApiPaginatedResponse(UnifiedAccountingPhonenumberOutput) @UseGuards(ApiKeyAuthGuard) @Get() diff --git a/packages/api/src/accounting/phonenumber/services/phonenumber.service.ts b/packages/api/src/accounting/phonenumber/services/phonenumber.service.ts index 7f943a277..6bf7f5882 100644 --- a/packages/api/src/accounting/phonenumber/services/phonenumber.service.ts +++ b/packages/api/src/accounting/phonenumber/services/phonenumber.service.ts @@ -9,12 +9,8 @@ import { UnifiedAccountingPhonenumberInput, UnifiedAccountingPhonenumberOutput, } from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; -import { OriginalPhoneNumberOutput } from '@@core/utils/types/original/original.accounting'; - -import { IPhoneNumberService } from '../types'; @Injectable() export class PhoneNumberService { @@ -29,14 +25,75 @@ export class PhoneNumberService { } async getPhoneNumber( - id_phonenumbering_phonenumber: string, + id_acc_phone_number: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const phoneNumber = await this.prisma.acc_phone_numbers.findUnique({ + where: { id_acc_phone_number: id_acc_phone_number }, + }); + + if (!phoneNumber) { + throw new Error( + `Phone number with ID ${id_acc_phone_number} not found.`, + ); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: phoneNumber.id_acc_phone_number }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedPhoneNumber: UnifiedAccountingPhonenumberOutput = { + id: phoneNumber.id_acc_phone_number, + number: phoneNumber.number, + type: phoneNumber.type, + company_info_id: phoneNumber.id_acc_company_info, + contact_id: phoneNumber.id_acc_contact, + field_mappings: field_mappings, + created_at: phoneNumber.created_at, + modified_at: phoneNumber.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: phoneNumber.id_acc_phone_number }, + }); + unifiedPhoneNumber.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.phone_number.pull', + method: 'GET', + url: '/accounting/phone_number', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedPhoneNumber; + } catch (error) { + throw error; + } } async getPhoneNumbers( @@ -47,7 +104,84 @@ export class PhoneNumberService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingPhonenumberOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const phoneNumbers = await this.prisma.acc_phone_numbers.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_phone_number: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = phoneNumbers.length > limit; + if (hasNextPage) phoneNumbers.pop(); + + const unifiedPhoneNumbers = await Promise.all( + phoneNumbers.map(async (phoneNumber) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: phoneNumber.id_acc_phone_number }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedPhoneNumber: UnifiedAccountingPhonenumberOutput = { + id: phoneNumber.id_acc_phone_number, + number: phoneNumber.number, + type: phoneNumber.type, + company_info_id: phoneNumber.id_acc_company_info, + contact_id: phoneNumber.id_acc_contact, + field_mappings: field_mappings, + created_at: phoneNumber.created_at, + modified_at: phoneNumber.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: phoneNumber.id_acc_phone_number }, + }); + unifiedPhoneNumber.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedPhoneNumber; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.phone_number.pull', + method: 'GET', + url: '/accounting/phone_numbers', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedPhoneNumbers, + next_cursor: hasNextPage + ? phoneNumbers[phoneNumbers.length - 1].id_acc_phone_number + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/phonenumber/sync/sync.service.ts b/packages/api/src/accounting/phonenumber/sync/sync.service.ts index 5d4ddd4ee..2d1e73807 100644 --- a/packages/api/src/accounting/phonenumber/sync/sync.service.ts +++ b/packages/api/src/accounting/phonenumber/sync/sync.service.ts @@ -1,7 +1,6 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; - import { Cron } from '@nestjs/schedule'; import { ApiResponse } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; @@ -11,6 +10,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedAccountingPhonenumberOutput } from '../types/model.unified'; import { IPhoneNumberService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_phone_numbers as AccPhoneNumber } from '@prisma/client'; +import { OriginalPhoneNumberOutput } from '@@core/utils/types/original/original.accounting'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -20,23 +25,137 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'phone_number', this); } async onModuleInit() { - // Initialization logic + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting phone numbers...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } } - saveToDb( + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IPhoneNumberService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingPhonenumberOutput, + OriginalPhoneNumberOutput, + IPhoneNumberService + >(integrationId, linkedUserId, 'accounting', 'phone_number', service, []); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + phoneNumbers: UnifiedAccountingPhonenumberOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); + ): Promise { + try { + const phoneNumberResults: AccPhoneNumber[] = []; + + for (let i = 0; i < phoneNumbers.length; i++) { + const phoneNumber = phoneNumbers[i]; + const originId = phoneNumber.remote_id; + + let existingPhoneNumber = await this.prisma.acc_phone_numbers.findFirst( + { + where: { + remote_id: originId, + id_connection: connection_id, + }, + }, + ); + + const phoneNumberData = { + number: phoneNumber.number, + type: phoneNumber.type, + id_acc_company_info: phoneNumber.company_info_id, + id_acc_contact: phoneNumber.contact_id, + modified_at: new Date(), + }; + + if (existingPhoneNumber) { + existingPhoneNumber = await this.prisma.acc_phone_numbers.update({ + where: { + id_acc_phone_number: existingPhoneNumber.id_acc_phone_number, + }, + data: phoneNumberData, + }); + } else { + existingPhoneNumber = await this.prisma.acc_phone_numbers.create({ + data: { + ...phoneNumberData, + id_acc_phone_number: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + phoneNumberResults.push(existingPhoneNumber); + + // Process field mappings + await this.ingestService.processFieldMappings( + phoneNumber.field_mappings, + existingPhoneNumber.id_acc_phone_number, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingPhoneNumber.id_acc_phone_number, + remote_data[i], + ); + } + + return phoneNumberResults; + } catch (error) { + throw error; + } } - // Additional methods and logic } diff --git a/packages/api/src/accounting/phonenumber/types/index.ts b/packages/api/src/accounting/phonenumber/types/index.ts index 74525dd7c..c2c962a38 100644 --- a/packages/api/src/accounting/phonenumber/types/index.ts +++ b/packages/api/src/accounting/phonenumber/types/index.ts @@ -5,6 +5,7 @@ import { } from './model.unified'; import { OriginalPhoneNumberOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IPhoneNumberService { addPhoneNumber( @@ -12,10 +13,7 @@ export interface IPhoneNumberService { linkedUserId: string, ): Promise>; - syncPhoneNumbers( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IPhoneNumberMapper { @@ -34,5 +32,7 @@ export interface IPhoneNumberMapper { slug: string; remote_id: string; }[], - ): Promise; + ): Promise< + UnifiedAccountingPhonenumberOutput | UnifiedAccountingPhonenumberOutput[] + >; } diff --git a/packages/api/src/accounting/phonenumber/types/model.unified.ts b/packages/api/src/accounting/phonenumber/types/model.unified.ts index 952614096..fe8cfedd8 100644 --- a/packages/api/src/accounting/phonenumber/types/model.unified.ts +++ b/packages/api/src/accounting/phonenumber/types/model.unified.ts @@ -1,3 +1,113 @@ -export class UnifiedAccountingPhonenumberInput {} +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsUUID, IsOptional, IsString, IsDateString } from 'class-validator'; -export class UnifiedAccountingPhonenumberOutput extends UnifiedAccountingPhonenumberInput {} +export class UnifiedAccountingPhonenumberInput { + @ApiPropertyOptional({ + type: String, + example: '+1234567890', + nullable: true, + description: 'The phone number', + }) + @IsString() + @IsOptional() + number?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Mobile', + nullable: true, + description: 'The type of phone number', + }) + @IsString() + @IsOptional() + type?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company info', + }) + @IsUUID() + @IsOptional() + company_info_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: false, + description: 'The UUID of the associated contact', + }) + @IsUUID() + contact_id: string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingPhonenumberOutput extends UnifiedAccountingPhonenumberInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the phone number record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'phone_1234', + nullable: true, + description: + 'The remote ID of the phone number in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the phone number in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the phone number record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the phone number record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} diff --git a/packages/api/src/accounting/purchaseorder/purchaseorder.controller.ts b/packages/api/src/accounting/purchaseorder/purchaseorder.controller.ts index e94ca049a..72623e99a 100644 --- a/packages/api/src/accounting/purchaseorder/purchaseorder.controller.ts +++ b/packages/api/src/accounting/purchaseorder/purchaseorder.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -34,7 +36,6 @@ import { ApiPostCustomResponse, } from '@@core/utils/dtos/openapi.respone.dto'; - @ApiTags('accounting/purchaseorders') @Controller('accounting/purchaseorders') export class PurchaseOrderController { @@ -58,6 +59,7 @@ export class PurchaseOrderController { }) @ApiPaginatedResponse(UnifiedAccountingPurchaseorderOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getPurchaseOrders( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/accounting/purchaseorder/services/purchaseorder.service.ts b/packages/api/src/accounting/purchaseorder/services/purchaseorder.service.ts index cbc0c7273..d40c9b4aa 100644 --- a/packages/api/src/accounting/purchaseorder/services/purchaseorder.service.ts +++ b/packages/api/src/accounting/purchaseorder/services/purchaseorder.service.ts @@ -1,20 +1,15 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; import { UnifiedAccountingPurchaseorderInput, UnifiedAccountingPurchaseorderOutput, } from '../types/model.unified'; - -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; -import { OriginalPurchaseOrderOutput } from '@@core/utils/types/original/original.accounting'; - -import { IPurchaseOrderService } from '../types'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class PurchaseOrderService { @@ -36,18 +31,182 @@ export class PurchaseOrderService { linkedUserId: string, remote_data?: boolean, ): Promise { - return; + try { + const service = this.serviceRegistry.getService(integrationId); + const resp = await service.addPurchaseOrder( + unifiedPurchaseOrderData, + linkedUserId, + ); + + const savedPurchaseOrder = await this.prisma.acc_purchase_orders.create({ + data: { + id_acc_purchase_order: uuidv4(), + ...unifiedPurchaseOrderData, + total_amount: unifiedPurchaseOrderData.total_amount + ? Number(unifiedPurchaseOrderData.total_amount) + : null, + remote_id: resp.data.remote_id, + id_connection: connection_id, + created_at: new Date(), + modified_at: new Date(), + }, + }); + + // Save line items + if (unifiedPurchaseOrderData.line_items) { + await Promise.all( + unifiedPurchaseOrderData.line_items.map(async (lineItem) => { + await this.prisma.acc_purchase_orders_line_items.create({ + data: { + id_acc_purchase_orders_line_item: uuidv4(), + id_acc_purchase_order: savedPurchaseOrder.id_acc_purchase_order, + ...lineItem, + unit_price: lineItem.unit_price + ? Number(lineItem.unit_price) + : null, + quantity: lineItem.quantity ? Number(lineItem.quantity) : null, + tax_amount: lineItem.tax_amount + ? Number(lineItem.tax_amount) + : null, + total_line_amount: lineItem.total_line_amount + ? Number(lineItem.total_line_amount) + : null, + created_at: new Date(), + modified_at: new Date(), + }, + }); + }), + ); + } + + const result: UnifiedAccountingPurchaseorderOutput = { + ...savedPurchaseOrder, + currency: savedPurchaseOrder.currency as CurrencyCode, + id: savedPurchaseOrder.id_acc_purchase_order, + total_amount: savedPurchaseOrder.total_amount + ? Number(savedPurchaseOrder.total_amount) + : undefined, + line_items: unifiedPurchaseOrderData.line_items, + }; + + if (remote_data) { + result.remote_data = resp.data; + } + + return result; + } catch (error) { + throw error; + } } async getPurchaseOrder( - id_purchaseordering_purchaseorder: string, + id_acc_purchase_order: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const purchaseOrder = await this.prisma.acc_purchase_orders.findUnique({ + where: { id_acc_purchase_order: id_acc_purchase_order }, + }); + + if (!purchaseOrder) { + throw new Error( + `Purchase order with ID ${id_acc_purchase_order} not found.`, + ); + } + + const lineItems = + await this.prisma.acc_purchase_orders_line_items.findMany({ + where: { id_acc_purchase_order: id_acc_purchase_order }, + }); + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: purchaseOrder.id_acc_purchase_order }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedPurchaseOrder: UnifiedAccountingPurchaseorderOutput = { + id: purchaseOrder.id_acc_purchase_order, + status: purchaseOrder.status, + issue_date: purchaseOrder.issue_date, + purchase_order_number: purchaseOrder.purchase_order_number, + delivery_date: purchaseOrder.delivery_date, + delivery_address: purchaseOrder.delivery_address, + customer: purchaseOrder.customer, + vendor: purchaseOrder.vendor, + memo: purchaseOrder.memo, + company_id: purchaseOrder.company, + total_amount: purchaseOrder.total_amount + ? Number(purchaseOrder.total_amount) + : undefined, + currency: purchaseOrder.currency as CurrencyCode, + exchange_rate: purchaseOrder.exchange_rate, + tracking_categories: purchaseOrder.tracking_categories, + accounting_period_id: purchaseOrder.id_acc_accounting_period, + field_mappings: field_mappings, + remote_id: purchaseOrder.remote_id, + remote_created_at: purchaseOrder.remote_created_at, + remote_updated_at: purchaseOrder.remote_updated_at, + created_at: purchaseOrder.created_at, + modified_at: purchaseOrder.modified_at, + line_items: lineItems.map((item) => ({ + id: item.id_acc_purchase_orders_line_item, + description: item.description, + unit_price: item.unit_price ? Number(item.unit_price) : undefined, + quantity: item.quantity ? Number(item.quantity) : undefined, + tracking_categories: item.tracking_categories, + tax_amount: item.tax_amount ? Number(item.tax_amount) : undefined, + total_line_amount: item.total_line_amount + ? Number(item.total_line_amount) + : undefined, + currency: item.currency as CurrencyCode, + exchange_rate: item.exchange_rate, + id_acc_account: item.id_acc_account, + id_acc_company: item.id_acc_company, + remote_id: item.remote_id, + created_at: item.created_at, + modified_at: item.modified_at, + })), + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: purchaseOrder.id_acc_purchase_order }, + }); + unifiedPurchaseOrder.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.purchase_order.pull', + method: 'GET', + url: '/accounting/purchase_order', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedPurchaseOrder; + } catch (error) { + throw error; + } } async getPurchaseOrders( @@ -58,7 +217,128 @@ export class PurchaseOrderService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingPurchaseorderOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const purchaseOrders = await this.prisma.acc_purchase_orders.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_purchase_order: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = purchaseOrders.length > limit; + if (hasNextPage) purchaseOrders.pop(); + + const unifiedPurchaseOrders = await Promise.all( + purchaseOrders.map(async (purchaseOrder) => { + const lineItems = + await this.prisma.acc_purchase_orders_line_items.findMany({ + where: { + id_acc_purchase_order: purchaseOrder.id_acc_purchase_order, + }, + }); + + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: purchaseOrder.id_acc_purchase_order, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedPurchaseOrder: UnifiedAccountingPurchaseorderOutput = { + id: purchaseOrder.id_acc_purchase_order, + status: purchaseOrder.status, + issue_date: purchaseOrder.issue_date, + purchase_order_number: purchaseOrder.purchase_order_number, + delivery_date: purchaseOrder.delivery_date, + delivery_address: purchaseOrder.delivery_address, + customer: purchaseOrder.customer, + vendor: purchaseOrder.vendor, + memo: purchaseOrder.memo, + company_id: purchaseOrder.company, + total_amount: purchaseOrder.total_amount + ? Number(purchaseOrder.total_amount) + : undefined, + currency: purchaseOrder.currency as CurrencyCode, + exchange_rate: purchaseOrder.exchange_rate, + tracking_categories: purchaseOrder.tracking_categories, + accounting_period_id: purchaseOrder.id_acc_accounting_period, + field_mappings: field_mappings, + remote_id: purchaseOrder.remote_id, + remote_created_at: purchaseOrder.remote_created_at, + remote_updated_at: purchaseOrder.remote_updated_at, + created_at: purchaseOrder.created_at, + modified_at: purchaseOrder.modified_at, + line_items: lineItems.map((item) => ({ + id: item.id_acc_purchase_orders_line_item, + description: item.description, + unit_price: item.unit_price ? Number(item.unit_price) : undefined, + quantity: item.quantity ? Number(item.quantity) : undefined, + tracking_categories: item.tracking_categories, + tax_amount: item.tax_amount ? Number(item.tax_amount) : undefined, + total_line_amount: item.total_line_amount + ? Number(item.total_line_amount) + : undefined, + currency: item.currency as CurrencyCode, + exchange_rate: item.exchange_rate, + id_acc_account: item.id_acc_account, + id_acc_company: item.id_acc_company, + remote_id: item.remote_id, + created_at: item.created_at, + modified_at: item.modified_at, + })), + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: purchaseOrder.id_acc_purchase_order, + }, + }); + unifiedPurchaseOrder.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedPurchaseOrder; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.purchase_order.pull', + method: 'GET', + url: '/accounting/purchase_orders', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedPurchaseOrders, + next_cursor: hasNextPage + ? purchaseOrders[purchaseOrders.length - 1].id_acc_purchase_order + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/purchaseorder/sync/sync.service.ts b/packages/api/src/accounting/purchaseorder/sync/sync.service.ts index 5bb870aac..f5e75eeb3 100644 --- a/packages/api/src/accounting/purchaseorder/sync/sync.service.ts +++ b/packages/api/src/accounting/purchaseorder/sync/sync.service.ts @@ -1,16 +1,24 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; - +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { OriginalPurchaseOrderOutput } from '@@core/utils/types/original/original.accounting'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_purchase_orders as AccPurchaseOrder } from '@prisma/client'; import { v4 as uuidv4 } from 'uuid'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { UnifiedAccountingPurchaseorderOutput } from '../types/model.unified'; import { IPurchaseOrderService } from '../types'; -import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { + LineItem, + UnifiedAccountingPurchaseorderOutput, +} from '../types/model.unified'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -20,22 +28,229 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'purchase_order', this); } async onModuleInit() { - // Initialization logic + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting purchase orders...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IPurchaseOrderService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingPurchaseorderOutput, + OriginalPurchaseOrderOutput, + IPurchaseOrderService + >( + integrationId, + linkedUserId, + 'accounting', + 'purchase_order', + service, + [], + ); + } catch (error) { + throw error; + } } - saveToDb( + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + purchaseOrders: UnifiedAccountingPurchaseorderOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); + ): Promise { + try { + const purchaseOrderResults: AccPurchaseOrder[] = []; + + for (let i = 0; i < purchaseOrders.length; i++) { + const purchaseOrder = purchaseOrders[i]; + const originId = purchaseOrder.remote_id; + + let existingPurchaseOrder = + await this.prisma.acc_purchase_orders.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const purchaseOrderData = { + status: purchaseOrder.status, + issue_date: purchaseOrder.issue_date, + purchase_order_number: purchaseOrder.purchase_order_number, + delivery_date: purchaseOrder.delivery_date, + delivery_address: purchaseOrder.delivery_address, + customer: purchaseOrder.customer, + vendor: purchaseOrder.vendor, + memo: purchaseOrder.memo, + company: purchaseOrder.company_id, + total_amount: purchaseOrder.total_amount + ? Number(purchaseOrder.total_amount) + : null, + currency: purchaseOrder.currency as CurrencyCode, + exchange_rate: purchaseOrder.exchange_rate, + tracking_categories: purchaseOrder.tracking_categories, + remote_created_at: purchaseOrder.remote_created_at, + remote_updated_at: purchaseOrder.remote_updated_at, + id_acc_accounting_period: purchaseOrder.accounting_period_id, + remote_id: originId, + modified_at: new Date(), + }; + + if (existingPurchaseOrder) { + existingPurchaseOrder = await this.prisma.acc_purchase_orders.update({ + where: { + id_acc_purchase_order: + existingPurchaseOrder.id_acc_purchase_order, + }, + data: purchaseOrderData, + }); + } else { + existingPurchaseOrder = await this.prisma.acc_purchase_orders.create({ + data: { + ...purchaseOrderData, + id_acc_purchase_order: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + purchaseOrderResults.push(existingPurchaseOrder); + + // Process field mappings + await this.ingestService.processFieldMappings( + purchaseOrder.field_mappings, + existingPurchaseOrder.id_acc_purchase_order, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingPurchaseOrder.id_acc_purchase_order, + remote_data[i], + ); + + // Handle line items + if (purchaseOrder.line_items && purchaseOrder.line_items.length > 0) { + await this.processPurchaseOrderLineItems( + existingPurchaseOrder.id_acc_purchase_order, + purchaseOrder.line_items, + ); + } + } + + return purchaseOrderResults; + } catch (error) { + throw error; + } + } + + private async processPurchaseOrderLineItems( + purchaseOrderId: string, + lineItems: LineItem[], + ): Promise { + for (const lineItem of lineItems) { + const lineItemData = { + description: lineItem.description, + unit_price: lineItem.unit_price ? Number(lineItem.unit_price) : null, + quantity: lineItem.quantity ? Number(lineItem.quantity) : null, + tracking_categories: lineItem.tracking_categories || [], + tax_amount: lineItem.tax_amount ? Number(lineItem.tax_amount) : null, + total_line_amount: lineItem.total_line_amount + ? Number(lineItem.total_line_amount) + : null, + currency: lineItem.currency as CurrencyCode, + exchange_rate: lineItem.exchange_rate, + id_acc_account: lineItem.account_id, + id_acc_company: lineItem.company_id, + remote_id: lineItem.remote_id, + modified_at: new Date(), + id_acc_purchase_order: purchaseOrderId, + }; + + const existingLineItem = + await this.prisma.acc_purchase_orders_line_items.findFirst({ + where: { + remote_id: lineItem.remote_id, + id_acc_purchase_order: purchaseOrderId, + }, + }); + + if (existingLineItem) { + await this.prisma.acc_purchase_orders_line_items.update({ + where: { + id_acc_purchase_orders_line_item: + existingLineItem.id_acc_purchase_orders_line_item, + }, + data: lineItemData, + }); + } else { + await this.prisma.acc_purchase_orders_line_items.create({ + data: { + ...lineItemData, + id_acc_purchase_orders_line_item: uuidv4(), + created_at: new Date(), + }, + }); + } + } + + // Remove any existing line items that are not in the current set + const currentRemoteIds = lineItems.map((item) => item.remote_id); + await this.prisma.acc_purchase_orders_line_items.deleteMany({ + where: { + id_acc_purchase_order: purchaseOrderId, + remote_id: { + notIn: currentRemoteIds, + }, + }, + }); } - // Additional methods and logic } diff --git a/packages/api/src/accounting/purchaseorder/types/index.ts b/packages/api/src/accounting/purchaseorder/types/index.ts index 396b042f6..d90499a97 100644 --- a/packages/api/src/accounting/purchaseorder/types/index.ts +++ b/packages/api/src/accounting/purchaseorder/types/index.ts @@ -5,6 +5,7 @@ import { } from './model.unified'; import { OriginalPurchaseOrderOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IPurchaseOrderService { addPurchaseOrder( @@ -12,10 +13,7 @@ export interface IPurchaseOrderService { linkedUserId: string, ): Promise>; - syncPurchaseOrders( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IPurchaseOrderMapper { @@ -34,5 +32,8 @@ export interface IPurchaseOrderMapper { slug: string; remote_id: string; }[], - ): Promise; + ): Promise< + | UnifiedAccountingPurchaseorderOutput + | UnifiedAccountingPurchaseorderOutput[] + >; } diff --git a/packages/api/src/accounting/purchaseorder/types/model.unified.ts b/packages/api/src/accounting/purchaseorder/types/model.unified.ts index 399c7bcd6..8d7de7efd 100644 --- a/packages/api/src/accounting/purchaseorder/types/model.unified.ts +++ b/packages/api/src/accounting/purchaseorder/types/model.unified.ts @@ -1,3 +1,388 @@ -export class UnifiedAccountingPurchaseorderInput {} +import { CurrencyCode } from '@@core/utils/types'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsNumber, + IsDateString, + IsArray, +} from 'class-validator'; -export class UnifiedAccountingPurchaseorderOutput extends UnifiedAccountingPurchaseorderInput {} +export class LineItem { + @ApiPropertyOptional({ + type: String, + example: 'Item description', + nullable: true, + description: 'Description of the line item', + }) + @IsString() + @IsOptional() + description?: string; + + @ApiPropertyOptional({ + type: Number, + example: 1000, + nullable: true, + description: 'The unit price of the item in cents', + }) + @IsNumber() + @IsOptional() + unit_price?: number; + + @ApiPropertyOptional({ + type: Number, + example: 5, + nullable: true, + description: 'The quantity of the item', + }) + @IsNumber() + @IsOptional() + quantity?: number; + + @ApiPropertyOptional({ + type: [String], + example: ['801f9ede-c698-4e66-a7fc-48d19eebaa4f'], + nullable: true, + description: + 'The UUIDs of the tracking categories associated with the line item', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tracking_categories?: string[]; + + @ApiPropertyOptional({ + type: Number, + example: 500, + nullable: true, + description: 'The tax amount for the line item in cents', + }) + @IsNumber() + @IsOptional() + tax_amount?: number; + + @ApiPropertyOptional({ + type: Number, + example: 5500, + nullable: true, + description: 'The total amount for the line item in cents', + }) + @IsNumber() + @IsOptional() + total_line_amount?: number; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + nullable: true, + enum: CurrencyCode, + description: 'The currency of the line item', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: String, + example: '1.0', + nullable: true, + description: 'The exchange rate for the line item', + }) + @IsString() + @IsOptional() + exchange_rate?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated account', + }) + @IsUUID() + @IsOptional() + account_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company', + }) + @IsUUID() + @IsOptional() + company_id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'remote_line_item_id_1234', + nullable: true, + description: 'The remote ID of the line item', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + description: 'The created date of the line item', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + description: 'The last modified date of the line item', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} + +export class UnifiedAccountingPurchaseorderInput { + @ApiPropertyOptional({ + type: String, + example: 'Pending', + nullable: true, + description: 'The status of the purchase order', + }) + @IsString() + @IsOptional() + status?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The issue date of the purchase order', + }) + @IsDateString() + @IsOptional() + issue_date?: Date; + + @ApiPropertyOptional({ + type: String, + example: 'PO-001', + nullable: true, + description: 'The purchase order number', + }) + @IsString() + @IsOptional() + purchase_order_number?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-07-15T12:00:00Z', + nullable: true, + description: 'The delivery date for the purchase order', + }) + @IsDateString() + @IsOptional() + delivery_date?: Date; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the delivery address', + }) + @IsUUID() + @IsOptional() + delivery_address?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the customer', + }) + @IsUUID() + @IsOptional() + customer?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the vendor', + }) + @IsUUID() + @IsOptional() + vendor?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Purchase order for Q3 inventory', + nullable: true, + description: 'A memo or note for the purchase order', + }) + @IsString() + @IsOptional() + memo?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the company', + }) + @IsUUID() + @IsOptional() + company_id?: string; + + @ApiPropertyOptional({ + type: Number, + example: 100000, + nullable: true, + description: 'The total amount of the purchase order in cents', + }) + @IsNumber() + @IsOptional() + total_amount?: number; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + enum: CurrencyCode, + nullable: true, + description: 'The currency of the purchase order', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: String, + example: '1.2', + nullable: true, + description: 'The exchange rate applied to the purchase order', + }) + @IsString() + @IsOptional() + exchange_rate?: string; + + @ApiPropertyOptional({ + type: [String], + example: ['801f9ede-c698-4e66-a7fc-48d19eebaa4f'], + nullable: true, + description: + 'The UUIDs of the tracking categories associated with the purchase order', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tracking_categories?: string[]; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated accounting period', + }) + @IsUUID() + @IsOptional() + accounting_period_id?: string; + + @ApiPropertyOptional({ + type: [LineItem], + description: 'The line items associated with this purchase order', + }) + @IsArray() + @IsOptional() + line_items?: LineItem[]; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingPurchaseorderOutput extends UnifiedAccountingPurchaseorderInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the purchase order record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'po_1234', + nullable: true, + description: + 'The remote ID of the purchase order in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: + 'The date when the purchase order was created in the remote system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: + 'The date when the purchase order was last updated in the remote system', + }) + @IsDateString() + @IsOptional() + remote_updated_at?: Date; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the purchase order in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the purchase order record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the purchase order record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} diff --git a/packages/api/src/accounting/taxrate/services/taxrate.service.ts b/packages/api/src/accounting/taxrate/services/taxrate.service.ts index cbff3b7d7..d0478dfed 100644 --- a/packages/api/src/accounting/taxrate/services/taxrate.service.ts +++ b/packages/api/src/accounting/taxrate/services/taxrate.service.ts @@ -1,20 +1,11 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { - UnifiedAccountingTaxrateInput, - UnifiedAccountingTaxrateOutput, -} from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; +import { UnifiedAccountingTaxrateOutput } from '../types/model.unified'; import { ServiceRegistry } from './registry.service'; -import { OriginalTaxRateOutput } from '@@core/utils/types/original/original.accounting'; - -import { ITaxRateService } from '../types'; @Injectable() export class TaxRateService { @@ -29,14 +20,78 @@ export class TaxRateService { } async getTaxRate( - id_taxrateing_taxrate: string, + id_acc_tax_rate: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const taxRate = await this.prisma.acc_tax_rates.findUnique({ + where: { id_acc_tax_rate: id_acc_tax_rate }, + }); + + if (!taxRate) { + throw new Error(`Tax rate with ID ${id_acc_tax_rate} not found.`); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: taxRate.id_acc_tax_rate }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedTaxRate: UnifiedAccountingTaxrateOutput = { + id: taxRate.id_acc_tax_rate, + description: taxRate.description, + total_tax_ratge: taxRate.total_tax_ratge + ? Number(taxRate.total_tax_ratge) + : undefined, + effective_tax_rate: taxRate.effective_tax_rate + ? Number(taxRate.effective_tax_rate) + : undefined, + company_id: taxRate.company, + field_mappings: field_mappings, + remote_id: taxRate.remote_id, + created_at: taxRate.created_at, + modified_at: taxRate.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: taxRate.id_acc_tax_rate }, + }); + unifiedTaxRate.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.tax_rate.pull', + method: 'GET', + url: '/accounting/tax_rate', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedTaxRate; + } catch (error) { + throw error; + } } async getTaxRates( @@ -47,7 +102,89 @@ export class TaxRateService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingTaxrateOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const taxRates = await this.prisma.acc_tax_rates.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_tax_rate: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = taxRates.length > limit; + if (hasNextPage) taxRates.pop(); + + const unifiedTaxRates = await Promise.all( + taxRates.map(async (taxRate) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: taxRate.id_acc_tax_rate }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedTaxRate: UnifiedAccountingTaxrateOutput = { + id: taxRate.id_acc_tax_rate, + description: taxRate.description, + total_tax_ratge: taxRate.total_tax_ratge + ? Number(taxRate.total_tax_ratge) + : undefined, + effective_tax_rate: taxRate.effective_tax_rate + ? Number(taxRate.effective_tax_rate) + : undefined, + company_id: taxRate.company, + field_mappings: field_mappings, + remote_id: taxRate.remote_id, + created_at: taxRate.created_at, + modified_at: taxRate.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: taxRate.id_acc_tax_rate }, + }); + unifiedTaxRate.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedTaxRate; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.tax_rate.pull', + method: 'GET', + url: '/accounting/tax_rates', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedTaxRates, + next_cursor: hasNextPage + ? taxRates[taxRates.length - 1].id_acc_tax_rate + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/taxrate/sync/sync.service.ts b/packages/api/src/accounting/taxrate/sync/sync.service.ts index 4de53339a..9c737358d 100644 --- a/packages/api/src/accounting/taxrate/sync/sync.service.ts +++ b/packages/api/src/accounting/taxrate/sync/sync.service.ts @@ -1,7 +1,6 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; - import { Cron } from '@nestjs/schedule'; import { ApiResponse } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; @@ -11,6 +10,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedAccountingTaxrateOutput } from '../types/model.unified'; import { ITaxRateService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_tax_rates as AccTaxRate } from '@prisma/client'; +import { OriginalTaxRateOutput } from '@@core/utils/types/original/original.accounting'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -20,22 +25,138 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'tax_rate', this); } async onModuleInit() { - // Initialization logic + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting tax rates...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } } - saveToDb( + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: ITaxRateService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingTaxrateOutput, + OriginalTaxRateOutput, + ITaxRateService + >(integrationId, linkedUserId, 'accounting', 'tax_rate', service, []); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + taxRates: UnifiedAccountingTaxrateOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); + ): Promise { + try { + const taxRateResults: AccTaxRate[] = []; + + for (let i = 0; i < taxRates.length; i++) { + const taxRate = taxRates[i]; + const originId = taxRate.remote_id; + + let existingTaxRate = await this.prisma.acc_tax_rates.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const taxRateData = { + description: taxRate.description, + total_tax_ratge: taxRate.total_tax_ratge + ? Number(taxRate.total_tax_ratge) + : null, + effective_tax_rate: taxRate.effective_tax_rate + ? Number(taxRate.effective_tax_rate) + : null, + company: taxRate.company_id, + remote_id: originId, + modified_at: new Date(), + }; + + if (existingTaxRate) { + existingTaxRate = await this.prisma.acc_tax_rates.update({ + where: { id_acc_tax_rate: existingTaxRate.id_acc_tax_rate }, + data: taxRateData, + }); + } else { + existingTaxRate = await this.prisma.acc_tax_rates.create({ + data: { + ...taxRateData, + id_acc_tax_rate: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + taxRateResults.push(existingTaxRate); + + // Process field mappings + await this.ingestService.processFieldMappings( + taxRate.field_mappings, + existingTaxRate.id_acc_tax_rate, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingTaxRate.id_acc_tax_rate, + remote_data[i], + ); + } + + return taxRateResults; + } catch (error) { + throw error; + } } - // Additional methods and logic } diff --git a/packages/api/src/accounting/taxrate/taxrate.controller.ts b/packages/api/src/accounting/taxrate/taxrate.controller.ts index e00835477..f48fa30a9 100644 --- a/packages/api/src/accounting/taxrate/taxrate.controller.ts +++ b/packages/api/src/accounting/taxrate/taxrate.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -28,8 +30,10 @@ import { import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { QueryDto } from '@@core/utils/dtos/query.dto'; -import { ApiGetCustomResponse, ApiPaginatedResponse } from '@@core/utils/dtos/openapi.respone.dto'; - +import { + ApiGetCustomResponse, + ApiPaginatedResponse, +} from '@@core/utils/dtos/openapi.respone.dto'; @ApiTags('accounting/taxrates') @Controller('accounting/taxrates') @@ -54,6 +58,7 @@ export class TaxRateController { }) @ApiPaginatedResponse(UnifiedAccountingTaxrateOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getTaxRates( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/accounting/taxrate/types/index.ts b/packages/api/src/accounting/taxrate/types/index.ts index e56e41e65..2e0b8104c 100644 --- a/packages/api/src/accounting/taxrate/types/index.ts +++ b/packages/api/src/accounting/taxrate/types/index.ts @@ -1,7 +1,11 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { UnifiedAccountingTaxrateInput, UnifiedAccountingTaxrateOutput } from './model.unified'; +import { + UnifiedAccountingTaxrateInput, + UnifiedAccountingTaxrateOutput, +} from './model.unified'; import { OriginalTaxRateOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface ITaxRateService { addTaxRate( @@ -9,10 +13,7 @@ export interface ITaxRateService { linkedUserId: string, ): Promise>; - syncTaxRates( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface ITaxRateMapper { diff --git a/packages/api/src/accounting/taxrate/types/model.unified.ts b/packages/api/src/accounting/taxrate/types/model.unified.ts index df05a1f44..ae3da9762 100644 --- a/packages/api/src/accounting/taxrate/types/model.unified.ts +++ b/packages/api/src/accounting/taxrate/types/model.unified.ts @@ -1,3 +1,120 @@ -export class UnifiedAccountingTaxrateInput {} +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsNumber, + IsDateString, +} from 'class-validator'; -export class UnifiedAccountingTaxrateOutput extends UnifiedAccountingTaxrateInput {} +export class UnifiedAccountingTaxrateInput { + @ApiPropertyOptional({ + type: String, + example: 'VAT 20%', + nullable: true, + description: 'The description of the tax rate', + }) + @IsString() + @IsOptional() + description?: string; + + @ApiPropertyOptional({ + type: Number, + example: 2000, + nullable: true, + description: 'The total tax rate in basis points (e.g., 2000 for 20%)', + }) + @IsNumber() + @IsOptional() + total_tax_ratge?: number; + + @ApiPropertyOptional({ + type: Number, + example: 1900, + nullable: true, + description: 'The effective tax rate in basis points (e.g., 1900 for 19%)', + }) + @IsNumber() + @IsOptional() + effective_tax_rate?: number; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company', + }) + @IsUUID() + @IsOptional() + company_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingTaxrateOutput extends UnifiedAccountingTaxrateInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the tax rate record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'tax_rate_1234', + nullable: true, + description: + 'The remote ID of the tax rate in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the tax rate in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the tax rate record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the tax rate record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} diff --git a/packages/api/src/accounting/trackingcategory/services/trackingcategory.service.ts b/packages/api/src/accounting/trackingcategory/services/trackingcategory.service.ts index c46282a5c..82cd563f6 100644 --- a/packages/api/src/accounting/trackingcategory/services/trackingcategory.service.ts +++ b/packages/api/src/accounting/trackingcategory/services/trackingcategory.service.ts @@ -1,20 +1,11 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { - UnifiedAccountingTrackingcategoryInput, - UnifiedAccountingTrackingcategoryOutput, -} from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; +import { UnifiedAccountingTrackingcategoryOutput } from '../types/model.unified'; import { ServiceRegistry } from './registry.service'; -import { OriginalTrackingCategoryOutput } from '@@core/utils/types/original/original.accounting'; - -import { ITrackingCategoryService } from '../types'; @Injectable() export class TrackingCategoryService { @@ -29,17 +20,84 @@ export class TrackingCategoryService { } async getTrackingCategory( - id_trackingcategorying_trackingcategory: string, + id_acc_tracking_category: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const trackingCategory = + await this.prisma.acc_tracking_categories.findUnique({ + where: { id_acc_tracking_category: id_acc_tracking_category }, + }); + + if (!trackingCategory) { + throw new Error( + `Tracking category with ID ${id_acc_tracking_category} not found.`, + ); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: trackingCategory.id_acc_tracking_category, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedTrackingCategory: UnifiedAccountingTrackingcategoryOutput = { + id: trackingCategory.id_acc_tracking_category, + name: trackingCategory.name, + status: trackingCategory.status, + category_type: trackingCategory.category_type, + parent_category: trackingCategory.parent_category, + field_mappings: field_mappings, + remote_id: trackingCategory.remote_id, + created_at: trackingCategory.created_at, + modified_at: trackingCategory.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: trackingCategory.id_acc_tracking_category, + }, + }); + unifiedTrackingCategory.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.tracking_category.pull', + method: 'GET', + url: '/accounting/tracking_category', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedTrackingCategory; + } catch (error) { + throw error; + } } - async getTrackingCategorys( + async getTrackingCategories( connectionId: string, projectId: string, integrationId: string, @@ -47,7 +105,92 @@ export class TrackingCategoryService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingTrackingcategoryOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const trackingCategories = + await this.prisma.acc_tracking_categories.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_tracking_category: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = trackingCategories.length > limit; + if (hasNextPage) trackingCategories.pop(); + + const unifiedTrackingCategories = await Promise.all( + trackingCategories.map(async (trackingCategory) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: trackingCategory.id_acc_tracking_category, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedTrackingCategory: UnifiedAccountingTrackingcategoryOutput = + { + id: trackingCategory.id_acc_tracking_category, + name: trackingCategory.name, + status: trackingCategory.status, + category_type: trackingCategory.category_type, + parent_category: trackingCategory.parent_category, + field_mappings: field_mappings, + remote_id: trackingCategory.remote_id, + created_at: trackingCategory.created_at, + modified_at: trackingCategory.modified_at, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: trackingCategory.id_acc_tracking_category, + }, + }); + unifiedTrackingCategory.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedTrackingCategory; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.tracking_category.pull', + method: 'GET', + url: '/accounting/tracking_categories', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedTrackingCategories, + next_cursor: hasNextPage + ? trackingCategories[trackingCategories.length - 1] + .id_acc_tracking_category + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/trackingcategory/sync/sync.service.ts b/packages/api/src/accounting/trackingcategory/sync/sync.service.ts index 128d54610..9ddc21bea 100644 --- a/packages/api/src/accounting/trackingcategory/sync/sync.service.ts +++ b/packages/api/src/accounting/trackingcategory/sync/sync.service.ts @@ -1,7 +1,6 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; - import { Cron } from '@nestjs/schedule'; import { ApiResponse } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; @@ -11,6 +10,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedAccountingTrackingcategoryOutput } from '../types/model.unified'; import { ITrackingCategoryService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_tracking_categories as AccTrackingCategory } from '@prisma/client'; +import { OriginalTrackingCategoryOutput } from '@@core/utils/types/original/original.accounting'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -20,22 +25,147 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'tracking_category', this); } async onModuleInit() { - // Initialization logic + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting tracking categories...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } } - saveToDb( + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: ITrackingCategoryService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingTrackingcategoryOutput, + OriginalTrackingCategoryOutput, + ITrackingCategoryService + >( + integrationId, + linkedUserId, + 'accounting', + 'tracking_category', + service, + [], + ); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + trackingCategories: UnifiedAccountingTrackingcategoryOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); + ): Promise { + try { + const trackingCategoryResults: AccTrackingCategory[] = []; + + for (let i = 0; i < trackingCategories.length; i++) { + const trackingCategory = trackingCategories[i]; + const originId = trackingCategory.remote_id; + + let existingTrackingCategory = + await this.prisma.acc_tracking_categories.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const trackingCategoryData = { + name: trackingCategory.name, + status: trackingCategory.status, + category_type: trackingCategory.category_type, + parent_category: trackingCategory.parent_category, + remote_id: originId, + modified_at: new Date(), + }; + + if (existingTrackingCategory) { + existingTrackingCategory = + await this.prisma.acc_tracking_categories.update({ + where: { + id_acc_tracking_category: + existingTrackingCategory.id_acc_tracking_category, + }, + data: trackingCategoryData, + }); + } else { + existingTrackingCategory = + await this.prisma.acc_tracking_categories.create({ + data: { + ...trackingCategoryData, + id_acc_tracking_category: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + trackingCategoryResults.push(existingTrackingCategory); + + // Process field mappings + await this.ingestService.processFieldMappings( + trackingCategory.field_mappings, + existingTrackingCategory.id_acc_tracking_category, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingTrackingCategory.id_acc_tracking_category, + remote_data[i], + ); + } + + return trackingCategoryResults; + } catch (error) { + throw error; + } } - // Additional methods and logic } diff --git a/packages/api/src/accounting/trackingcategory/trackingcategory.controller.ts b/packages/api/src/accounting/trackingcategory/trackingcategory.controller.ts index 14586690c..b88af1951 100644 --- a/packages/api/src/accounting/trackingcategory/trackingcategory.controller.ts +++ b/packages/api/src/accounting/trackingcategory/trackingcategory.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -28,8 +30,10 @@ import { import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { QueryDto } from '@@core/utils/dtos/query.dto'; -import { ApiGetCustomResponse, ApiPaginatedResponse } from '@@core/utils/dtos/openapi.respone.dto'; - +import { + ApiGetCustomResponse, + ApiPaginatedResponse, +} from '@@core/utils/dtos/openapi.respone.dto'; @ApiTags('accounting/trackingcategories') @Controller('accounting/trackingcategories') @@ -54,6 +58,7 @@ export class TrackingCategoryController { }) @ApiPaginatedResponse(UnifiedAccountingTrackingcategoryOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getTrackingCategorys( @Headers('x-connection-token') connection_token: string, @@ -65,7 +70,7 @@ export class TrackingCategoryController { connection_token, ); const { remote_data, limit, cursor } = query; - return this.trackingcategoryService.getTrackingCategorys( + return this.trackingcategoryService.getTrackingCategories( connectionId, projectId, remoteSource, diff --git a/packages/api/src/accounting/trackingcategory/types/index.ts b/packages/api/src/accounting/trackingcategory/types/index.ts index f45b7cc1a..1f118badc 100644 --- a/packages/api/src/accounting/trackingcategory/types/index.ts +++ b/packages/api/src/accounting/trackingcategory/types/index.ts @@ -5,6 +5,7 @@ import { } from './model.unified'; import { OriginalTrackingCategoryOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface ITrackingCategoryService { addTrackingCategory( @@ -12,10 +13,7 @@ export interface ITrackingCategoryService { linkedUserId: string, ): Promise>; - syncTrackingCategorys( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface ITrackingCategoryMapper { @@ -34,5 +32,8 @@ export interface ITrackingCategoryMapper { slug: string; remote_id: string; }[], - ): Promise; + ): Promise< + | UnifiedAccountingTrackingcategoryOutput + | UnifiedAccountingTrackingcategoryOutput[] + >; } diff --git a/packages/api/src/accounting/trackingcategory/types/model.unified.ts b/packages/api/src/accounting/trackingcategory/types/model.unified.ts index afb3085c3..99d21a0fe 100644 --- a/packages/api/src/accounting/trackingcategory/types/model.unified.ts +++ b/packages/api/src/accounting/trackingcategory/types/model.unified.ts @@ -1,3 +1,114 @@ -export class UnifiedAccountingTrackingcategoryInput {} +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsUUID, IsOptional, IsString, IsDateString } from 'class-validator'; -export class UnifiedAccountingTrackingcategoryOutput extends UnifiedAccountingTrackingcategoryInput {} +export class UnifiedAccountingTrackingcategoryInput { + @ApiPropertyOptional({ + type: String, + example: 'Department', + nullable: true, + description: 'The name of the tracking category', + }) + @IsString() + @IsOptional() + name?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Active', + nullable: true, + description: 'The status of the tracking category', + }) + @IsString() + @IsOptional() + status?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Expense', + nullable: true, + description: 'The type of the tracking category', + }) + @IsString() + @IsOptional() + category_type?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the parent category, if applicable', + }) + @IsUUID() + @IsOptional() + parent_category?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingTrackingcategoryOutput extends UnifiedAccountingTrackingcategoryInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the tracking category record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'tracking_category_1234', + nullable: true, + description: + 'The remote ID of the tracking category in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the tracking category in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the tracking category record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the tracking category record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; +} diff --git a/packages/api/src/accounting/transaction/services/transaction.service.ts b/packages/api/src/accounting/transaction/services/transaction.service.ts index 6fc38d448..ce87caf26 100644 --- a/packages/api/src/accounting/transaction/services/transaction.service.ts +++ b/packages/api/src/accounting/transaction/services/transaction.service.ts @@ -1,20 +1,12 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { - UnifiedAccountingTransactionInput, - UnifiedAccountingTransactionOutput, -} from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; +import { UnifiedAccountingTransactionOutput } from '../types/model.unified'; import { ServiceRegistry } from './registry.service'; -import { OriginalTransactionOutput } from '@@core/utils/types/original/original.accounting'; - -import { ITransactionService } from '../types'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class TransactionService { @@ -29,14 +21,103 @@ export class TransactionService { } async getTransaction( - id_transactioning_transaction: string, + id_acc_transaction: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const transaction = await this.prisma.acc_transactions.findUnique({ + where: { id_acc_transaction: id_acc_transaction }, + }); + + if (!transaction) { + throw new Error(`Transaction with ID ${id_acc_transaction} not found.`); + } + + const lineItems = await this.prisma.acc_transactions_lines_items.findMany( + { + where: { id_acc_transaction: id_acc_transaction }, + }, + ); + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: transaction.id_acc_transaction }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedTransaction: UnifiedAccountingTransactionOutput = { + id: transaction.id_acc_transaction, + transaction_type: transaction.transaction_type, + number: transaction.number ? Number(transaction.number) : undefined, + transaction_date: transaction.transaction_date, + total_amount: transaction.total_amount, + exchange_rate: transaction.exchange_rate, + currency: transaction.currency as CurrencyCode, + tracking_categories: transaction.tracking_categories, + account_id: transaction.id_acc_account, + contact_id: transaction.id_acc_contact, + company_info_id: transaction.id_acc_company_info, + accounting_period_id: transaction.id_acc_accounting_period, + field_mappings: field_mappings, + remote_id: transaction.remote_id, + created_at: transaction.created_at, + modified_at: transaction.modified_at, + line_items: lineItems.map((item) => ({ + memo: item.memo, + unit_price: item.unit_price, + quantity: item.quantity, + total_line_amount: item.total_line_amount, + id_acc_tax_rate: item.id_acc_tax_rate, + currency: item.currency as CurrencyCode, + exchange_rate: item.exchange_rate, + tracking_categories: item.tracking_categories, + id_acc_company_info: item.id_acc_company_info, + id_acc_item: item.id_acc_item, + id_acc_account: item.id_acc_account, + remote_id: item.remote_id, + created_at: item.created_at, + modified_at: item.modified_at, + })), + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: transaction.id_acc_transaction }, + }); + unifiedTransaction.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.transaction.pull', + method: 'GET', + url: '/accounting/transaction', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedTransaction; + } catch (error) { + throw error; + } } async getTransactions( @@ -47,7 +128,114 @@ export class TransactionService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingTransactionOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const transactions = await this.prisma.acc_transactions.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_transaction: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = transactions.length > limit; + if (hasNextPage) transactions.pop(); + + const unifiedTransactions = await Promise.all( + transactions.map(async (transaction) => { + const lineItems = + await this.prisma.acc_transactions_lines_items.findMany({ + where: { id_acc_transaction: transaction.id_acc_transaction }, + }); + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: transaction.id_acc_transaction }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedTransaction: UnifiedAccountingTransactionOutput = { + id: transaction.id_acc_transaction, + transaction_type: transaction.transaction_type, + number: transaction.number ? Number(transaction.number) : undefined, + transaction_date: transaction.transaction_date, + total_amount: transaction.total_amount, + exchange_rate: transaction.exchange_rate, + currency: transaction.currency as CurrencyCode, + tracking_categories: transaction.tracking_categories, + account_id: transaction.id_acc_account, + contact_id: transaction.id_acc_contact, + company_info_id: transaction.id_acc_company_info, + accounting_period_id: transaction.id_acc_accounting_period, + field_mappings: field_mappings, + remote_id: transaction.remote_id, + created_at: transaction.created_at, + modified_at: transaction.modified_at, + line_items: lineItems.map((item) => ({ + id: item.id_acc_transactions_lines_item, + memo: item.memo, + unit_price: item.unit_price, + quantity: item.quantity, + total_line_amount: item.total_line_amount, + id_acc_tax_rate: item.id_acc_tax_rate, + currency: item.currency as CurrencyCode, + exchange_rate: item.exchange_rate, + tracking_categories: item.tracking_categories, + id_acc_company_info: item.id_acc_company_info, + id_acc_item: item.id_acc_item, + id_acc_account: item.id_acc_account, + remote_id: item.remote_id, + created_at: item.created_at, + modified_at: item.modified_at, + })), + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: transaction.id_acc_transaction }, + }); + unifiedTransaction.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedTransaction; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.transaction.pull', + method: 'GET', + url: '/accounting/transactions', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedTransactions, + next_cursor: hasNextPage + ? transactions[transactions.length - 1].id_acc_transaction + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/transaction/sync/sync.service.ts b/packages/api/src/accounting/transaction/sync/sync.service.ts index c63c23b73..13f6d92a4 100644 --- a/packages/api/src/accounting/transaction/sync/sync.service.ts +++ b/packages/api/src/accounting/transaction/sync/sync.service.ts @@ -1,16 +1,24 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; - +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { OriginalTransactionOutput } from '@@core/utils/types/original/original.accounting'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_transactions as AccTransaction } from '@prisma/client'; import { v4 as uuidv4 } from 'uuid'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { UnifiedAccountingTransactionOutput } from '../types/model.unified'; import { ITransactionService } from '../types'; -import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { + LineItem, + UnifiedAccountingTransactionOutput, +} from '../types/model.unified'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -20,23 +28,212 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'transaction', this); } async onModuleInit() { - // Initialization logic + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting transactions...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: ITransactionService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingTransactionOutput, + OriginalTransactionOutput, + ITransactionService + >(integrationId, linkedUserId, 'accounting', 'transaction', service, []); + } catch (error) { + throw error; + } } - saveToDb( + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + transactions: UnifiedAccountingTransactionOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); + ): Promise { + try { + const transactionResults: AccTransaction[] = []; + + for (let i = 0; i < transactions.length; i++) { + const transaction = transactions[i]; + const originId = transaction.remote_id; + + let existingTransaction = await this.prisma.acc_transactions.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const transactionData = { + transaction_type: transaction.transaction_type, + number: transaction.number ? Number(transaction.number) : null, + transaction_date: transaction.transaction_date, + total_amount: transaction.total_amount, + exchange_rate: transaction.exchange_rate, + currency: transaction.currency as CurrencyCode, + tracking_categories: transaction.tracking_categories || [], + id_acc_account: transaction.account_id, + id_acc_contact: transaction.contact_id, + id_acc_company_info: transaction.company_info_id, + id_acc_accounting_period: transaction.accounting_period_id, + remote_id: originId, + modified_at: new Date(), + }; + + if (existingTransaction) { + existingTransaction = await this.prisma.acc_transactions.update({ + where: { + id_acc_transaction: existingTransaction.id_acc_transaction, + }, + data: transactionData, + }); + } else { + existingTransaction = await this.prisma.acc_transactions.create({ + data: { + ...transactionData, + id_acc_transaction: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + transactionResults.push(existingTransaction); + + // Process field mappings + await this.ingestService.processFieldMappings( + transaction.field_mappings, + existingTransaction.id_acc_transaction, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingTransaction.id_acc_transaction, + remote_data[i], + ); + + // Handle line items (acc_transactions_lines_items) + if (transaction.line_items && transaction.line_items.length > 0) { + await this.processLineItems( + existingTransaction.id_acc_transaction, + transaction.line_items, + ); + } + } + + return transactionResults; + } catch (error) { + throw error; + } } - // Additional methods and logic + private async processLineItems( + transactionId: string, + lineItems: LineItem[], + ): Promise { + for (const lineItem of lineItems) { + const lineItemData = { + memo: lineItem.memo, + unit_price: lineItem.unit_price, + quantity: lineItem.quantity, + total_line_amount: lineItem.total_line_amount, + tax_rate_id: lineItem.tax_rate_id, + currency: lineItem.currency as CurrencyCode, + exchange_rate: lineItem.exchange_rate, + tracking_categories: lineItem.tracking_categories || [], + id_acc_company_info: lineItem.company_info_id, + id_acc_item: lineItem.item_id, + id_acc_account: lineItem.account_id, + remote_id: lineItem.remote_id, + modified_at: new Date(), + id_acc_transaction: transactionId, + }; + + const existingLineItem = + await this.prisma.acc_transactions_lines_items.findFirst({ + where: { + remote_id: lineItem.remote_id, + id_acc_transaction: transactionId, + }, + }); + + if (existingLineItem) { + await this.prisma.acc_transactions_lines_items.update({ + where: { + id_acc_transactions_lines_item: + existingLineItem.id_acc_transactions_lines_item, + }, + data: lineItemData, + }); + } else { + await this.prisma.acc_transactions_lines_items.create({ + data: { + ...lineItemData, + id_acc_transactions_lines_item: uuidv4(), + created_at: new Date(), + }, + }); + } + } + + // Remove any existing line items that are not in the current set + const currentRemoteIds = lineItems.map((item) => item.remote_id); + await this.prisma.acc_transactions_lines_items.deleteMany({ + where: { + id_acc_transaction: transactionId, + remote_id: { + notIn: currentRemoteIds, + }, + }, + }); + } } diff --git a/packages/api/src/accounting/transaction/transaction.controller.ts b/packages/api/src/accounting/transaction/transaction.controller.ts index b34e99bcc..9c70f5599 100644 --- a/packages/api/src/accounting/transaction/transaction.controller.ts +++ b/packages/api/src/accounting/transaction/transaction.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -28,8 +30,10 @@ import { import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { QueryDto } from '@@core/utils/dtos/query.dto'; -import { ApiGetCustomResponse, ApiPaginatedResponse } from '@@core/utils/dtos/openapi.respone.dto'; - +import { + ApiGetCustomResponse, + ApiPaginatedResponse, +} from '@@core/utils/dtos/openapi.respone.dto'; @ApiTags('accounting/transactions') @Controller('accounting/transactions') @@ -54,6 +58,7 @@ export class TransactionController { }) @ApiPaginatedResponse(UnifiedAccountingTransactionOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getTransactions( @Headers('x-connection-token') connection_token: string, @@ -82,8 +87,7 @@ export class TransactionController { @ApiOperation({ operationId: 'retrieveAccountingTransaction', summary: 'Retrieve Transactions', - description: - 'Retrieve Transactions from any connected Accounting software', + description: 'Retrieve Transactions from any connected Accounting software', }) @ApiParam({ name: 'id', diff --git a/packages/api/src/accounting/transaction/types/index.ts b/packages/api/src/accounting/transaction/types/index.ts index ef214f1d0..56a15eaaf 100644 --- a/packages/api/src/accounting/transaction/types/index.ts +++ b/packages/api/src/accounting/transaction/types/index.ts @@ -5,6 +5,7 @@ import { } from './model.unified'; import { OriginalTransactionOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface ITransactionService { addTransaction( @@ -12,10 +13,7 @@ export interface ITransactionService { linkedUserId: string, ): Promise>; - syncTransactions( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface ITransactionMapper { @@ -34,5 +32,7 @@ export interface ITransactionMapper { slug: string; remote_id: string; }[], - ): Promise; + ): Promise< + UnifiedAccountingTransactionOutput | UnifiedAccountingTransactionOutput[] + >; } diff --git a/packages/api/src/accounting/transaction/types/model.unified.ts b/packages/api/src/accounting/transaction/types/model.unified.ts index 105ce9ae0..27e5aae97 100644 --- a/packages/api/src/accounting/transaction/types/model.unified.ts +++ b/packages/api/src/accounting/transaction/types/model.unified.ts @@ -1,3 +1,361 @@ -export class UnifiedAccountingTransactionInput {} +import { CurrencyCode } from '@@core/utils/types'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsNumber, + IsDateString, + IsArray, +} from 'class-validator'; -export class UnifiedAccountingTransactionOutput extends UnifiedAccountingTransactionInput {} +export class LineItem { + @ApiPropertyOptional({ + type: String, + example: 'Product description', + nullable: true, + description: 'Memo or description for the line item', + }) + @IsString() + @IsOptional() + memo?: string; + + @ApiPropertyOptional({ + type: String, + example: '10.99', + nullable: true, + description: 'Unit price of the item', + }) + @IsString() + @IsOptional() + unit_price?: string; + + @ApiPropertyOptional({ + type: String, + example: '2', + nullable: true, + description: 'Quantity of the item', + }) + @IsString() + @IsOptional() + quantity?: string; + + @ApiPropertyOptional({ + type: String, + example: '21.98', + nullable: true, + description: 'Total amount for the line item', + }) + @IsString() + @IsOptional() + total_line_amount?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated tax rate', + }) + @IsUUID() + @IsOptional() + tax_rate_id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + enum: CurrencyCode, + nullable: true, + description: 'The currency of the line item', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: String, + example: '1.0', + nullable: true, + description: 'The exchange rate for the line item', + }) + @IsString() + @IsOptional() + exchange_rate?: string; + + @ApiPropertyOptional({ + type: [String], + example: ['801f9ede-c698-4e66-a7fc-48d19eebaa4f'], + nullable: true, + description: + 'The UUIDs of tracking categories associated with the line item', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tracking_categories?: string[]; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company info', + }) + @IsUUID() + @IsOptional() + company_info_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated item', + }) + @IsUUID() + @IsOptional() + item_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated account', + }) + @IsUUID() + @IsOptional() + account_id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'remote_line_item_id_1234', + nullable: true, + description: 'The remote ID of the line item', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + description: 'The created date of the line item', + }) + @IsDateString() + created_at: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + description: 'The last modified date of the line item', + }) + @IsDateString() + modified_at: Date; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated transaction', + }) + @IsUUID() + @IsOptional() + transaction_id?: string; +} + +export class UnifiedAccountingTransactionInput { + @ApiPropertyOptional({ + type: String, + example: 'Sale', + nullable: true, + description: 'The type of the transaction', + }) + @IsString() + @IsOptional() + transaction_type?: string; + + @ApiPropertyOptional({ + type: String, + example: '1001', + nullable: true, + description: 'The transaction number', + }) + @IsNumber() + @IsOptional() + number?: number; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The date of the transaction', + }) + @IsDateString() + @IsOptional() + transaction_date?: Date; + + @ApiPropertyOptional({ + type: String, + example: '1000', + nullable: true, + description: 'The total amount of the transaction', + }) + @IsString() + @IsOptional() + total_amount?: string; + + @ApiPropertyOptional({ + type: String, + example: '1.2', + nullable: true, + description: 'The exchange rate applied to the transaction', + }) + @IsString() + @IsOptional() + exchange_rate?: string; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + enum: CurrencyCode, + nullable: true, + description: 'The currency of the transaction', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: [String], + example: ['801f9ede-c698-4e66-a7fc-48d19eebaa4f'], + nullable: true, + description: + 'The UUIDs of the tracking categories associated with the transaction', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tracking_categories?: string[]; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated account', + }) + @IsUUID() + @IsOptional() + account_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated contact', + }) + @IsUUID() + @IsOptional() + contact_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company info', + }) + @IsUUID() + @IsOptional() + company_info_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated accounting period', + }) + @IsUUID() + @IsOptional() + accounting_period_id?: string; + + @ApiPropertyOptional({ + type: [LineItem], + description: 'The line items associated with this transaction', + }) + @IsArray() + @IsOptional() + line_items?: LineItem[]; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingTransactionOutput extends UnifiedAccountingTransactionInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the transaction record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'remote_id_1234', + nullable: false, + description: 'The remote ID of the transaction', + }) + @IsString() + remote_id: string; // Required field + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: false, + description: 'The created date of the transaction', + }) + @IsDateString() + created_at: Date; // Required field + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the tracking category in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: false, + description: 'The last modified date of the transaction', + }) + @IsDateString() + modified_at: Date; // Required field + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: + 'The date when the transaction was last updated in the remote system', + }) + @IsDateString() + @IsOptional() + remote_updated_at?: Date; +} diff --git a/packages/api/src/accounting/vendorcredit/services/vendorcredit.service.ts b/packages/api/src/accounting/vendorcredit/services/vendorcredit.service.ts index 4f2bd2d82..838ac5bff 100644 --- a/packages/api/src/accounting/vendorcredit/services/vendorcredit.service.ts +++ b/packages/api/src/accounting/vendorcredit/services/vendorcredit.service.ts @@ -1,20 +1,12 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { - UnifiedAccountingVendorcreditInput, - UnifiedAccountingVendorcreditOutput, -} from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; +import { UnifiedAccountingVendorcreditOutput } from '../types/model.unified'; import { ServiceRegistry } from './registry.service'; -import { OriginalVendorCreditOutput } from '@@core/utils/types/original/original.accounting'; - -import { IVendorCreditService } from '../types'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class VendorCreditService { @@ -29,14 +21,100 @@ export class VendorCreditService { } async getVendorCredit( - id_vendorcrediting_vendorcredit: string, + id_acc_vendor_credit: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const vendorCredit = await this.prisma.acc_vendor_credits.findUnique({ + where: { id_acc_vendor_credit: id_acc_vendor_credit }, + }); + + if (!vendorCredit) { + throw new Error( + `Vendor credit with ID ${id_acc_vendor_credit} not found.`, + ); + } + + const lineItems = await this.prisma.acc_vendor_credit_lines.findMany({ + where: { id_acc_vendor_credit: id_acc_vendor_credit }, + }); + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: vendorCredit.id_acc_vendor_credit }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedVendorCredit: UnifiedAccountingVendorcreditOutput = { + id: vendorCredit.id_acc_vendor_credit, + number: vendorCredit.number, + transaction_date: vendorCredit.transaction_date, + vendor: vendorCredit.vendor, + total_amount: vendorCredit.total_amount + ? Number(vendorCredit.total_amount) + : undefined, + currency: vendorCredit.currency as CurrencyCode, + exchange_rate: vendorCredit.exchange_rate, + company_id: vendorCredit.company, + tracking_categories: vendorCredit.tracking_categories, + accounting_period_id: vendorCredit.accounting_period, + field_mappings: field_mappings, + remote_id: vendorCredit.remote_id, + created_at: vendorCredit.created_at.toISOString(), + modified_at: vendorCredit.modified_at, + line_items: lineItems.map((item) => ({ + id: item.id_acc_vendor_credit_line, + net_amount: item.net_amount ? item.net_amount.toString() : undefined, + tracking_categories: item.tracking_categories, + description: item.description, + id_acc_account: item.id_acc_account, + exchange_rate: item.exchange_rate, + id_acc_company_info: item.id_acc_company_info, + remote_id: item.remote_id, + created_at: item.created_at, + modified_at: item.modified_at, + id_acc_vendor_credit: item.id_acc_vendor_credit, + })), + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: vendorCredit.id_acc_vendor_credit }, + }); + unifiedVendorCredit.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.vendor_credit.pull', + method: 'GET', + url: '/accounting/vendor_credit', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedVendorCredit; + } catch (error) { + throw error; + } } async getVendorCredits( @@ -47,7 +125,111 @@ export class VendorCreditService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedAccountingVendorcreditOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const vendorCredits = await this.prisma.acc_vendor_credits.findMany({ + take: limit + 1, + cursor: cursor ? { id_acc_vendor_credit: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = vendorCredits.length > limit; + if (hasNextPage) vendorCredits.pop(); + + const unifiedVendorCredits = await Promise.all( + vendorCredits.map(async (vendorCredit) => { + const lineItems = await this.prisma.acc_vendor_credit_lines.findMany({ + where: { id_acc_vendor_credit: vendorCredit.id_acc_vendor_credit }, + }); + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: vendorCredit.id_acc_vendor_credit }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedVendorCredit: UnifiedAccountingVendorcreditOutput = { + id: vendorCredit.id_acc_vendor_credit, + number: vendorCredit.number, + transaction_date: vendorCredit.transaction_date, + vendor: vendorCredit.vendor, + total_amount: vendorCredit.total_amount + ? Number(vendorCredit.total_amount) + : undefined, + currency: vendorCredit.currency as CurrencyCode as CurrencyCode, + exchange_rate: vendorCredit.exchange_rate, + company_id: vendorCredit.company, + tracking_categories: vendorCredit.tracking_categories, + accounting_period_id: vendorCredit.accounting_period, + field_mappings: field_mappings, + remote_id: vendorCredit.remote_id, + created_at: vendorCredit.created_at.toISOString(), + modified_at: vendorCredit.modified_at, + line_items: lineItems.map((item) => ({ + id: item.id_acc_vendor_credit_line, + net_amount: item.net_amount + ? item.net_amount.toString() + : undefined, + tracking_categories: item.tracking_categories, + description: item.description, + id_acc_account: item.id_acc_account, + exchange_rate: item.exchange_rate, + id_acc_company_info: item.id_acc_company_info, + remote_id: item.remote_id, + created_at: item.created_at, + modified_at: item.modified_at, + id_acc_vendor_credit: item.id_acc_vendor_credit, + })), + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: vendorCredit.id_acc_vendor_credit }, + }); + unifiedVendorCredit.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedVendorCredit; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'accounting.vendor_credit.pull', + method: 'GET', + url: '/accounting/vendor_credits', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedVendorCredits, + next_cursor: hasNextPage + ? vendorCredits[vendorCredits.length - 1].id_acc_vendor_credit + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/accounting/vendorcredit/sync/sync.service.ts b/packages/api/src/accounting/vendorcredit/sync/sync.service.ts index c7b7fa339..c55697a27 100644 --- a/packages/api/src/accounting/vendorcredit/sync/sync.service.ts +++ b/packages/api/src/accounting/vendorcredit/sync/sync.service.ts @@ -1,16 +1,24 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; - +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { OriginalVendorCreditOutput } from '@@core/utils/types/original/original.accounting'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { ACCOUNTING_PROVIDERS } from '@panora/shared'; +import { acc_vendor_credits as AccVendorCredit } from '@prisma/client'; import { v4 as uuidv4 } from 'uuid'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { UnifiedAccountingVendorcreditOutput } from '../types/model.unified'; import { IVendorCreditService } from '../types'; -import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { + UnifiedAccountingVendorcreditOutput, + LineItem, +} from '../types/model.unified'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -20,22 +28,215 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('accounting', 'vendor_credit', this); } async onModuleInit() { - // Initialization logic + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing accounting vendor credits...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of ACCOUNTING_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IVendorCreditService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedAccountingVendorcreditOutput, + OriginalVendorCreditOutput, + IVendorCreditService + >( + integrationId, + linkedUserId, + 'accounting', + 'vendor_credit', + service, + [], + ); + } catch (error) { + throw error; + } } - saveToDb( + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + vendorCredits: UnifiedAccountingVendorcreditOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); + ): Promise { + try { + const vendorCreditResults: AccVendorCredit[] = []; + + for (let i = 0; i < vendorCredits.length; i++) { + const vendorCredit = vendorCredits[i]; + const originId = vendorCredit.remote_id; + + let existingVendorCredit = + await this.prisma.acc_vendor_credits.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const vendorCreditData = { + number: vendorCredit.number, + transaction_date: vendorCredit.transaction_date, + vendor: vendorCredit.vendor, + total_amount: vendorCredit.total_amount + ? Number(vendorCredit.total_amount) + : null, + currency: vendorCredit.currency as CurrencyCode, + exchange_rate: vendorCredit.exchange_rate, + id_acc_company: vendorCredit.company_id, + tracking_categories: vendorCredit.tracking_categories || [], + id_acc_accounting_period: vendorCredit.accounting_period_id, + remote_id: originId, + modified_at: new Date(), + }; + + if (existingVendorCredit) { + existingVendorCredit = await this.prisma.acc_vendor_credits.update({ + where: { + id_acc_vendor_credit: existingVendorCredit.id_acc_vendor_credit, + }, + data: vendorCreditData, + }); + } else { + existingVendorCredit = await this.prisma.acc_vendor_credits.create({ + data: { + ...vendorCreditData, + id_acc_vendor_credit: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + vendorCreditResults.push(existingVendorCredit); + + // Process field mappings + await this.ingestService.processFieldMappings( + vendorCredit.field_mappings, + existingVendorCredit.id_acc_vendor_credit, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingVendorCredit.id_acc_vendor_credit, + remote_data[i], + ); + + // Handle line items + if (vendorCredit.line_items && vendorCredit.line_items.length > 0) { + await this.processVendorCreditLineItems( + existingVendorCredit.id_acc_vendor_credit, + vendorCredit.line_items, + ); + } + } + + return vendorCreditResults; + } catch (error) { + throw error; + } + } + + private async processVendorCreditLineItems( + vendorCreditId: string, + lineItems: LineItem[], + ): Promise { + for (const lineItem of lineItems) { + const lineItemData = { + net_amount: lineItem.net_amount ? Number(lineItem.net_amount) : null, + tracking_categories: lineItem.tracking_categories || [], + description: lineItem.description, + id_acc_account: lineItem.account_id, + exchange_rate: lineItem.exchange_rate, + id_acc_company_info: lineItem.company_info_id, + remote_id: lineItem.remote_id, + modified_at: new Date(), + id_acc_vendor_credit: vendorCreditId, + }; + + const existingLineItem = + await this.prisma.acc_vendor_credit_lines.findFirst({ + where: { + remote_id: lineItem.remote_id, + id_acc_vendor_credit: vendorCreditId, + }, + }); + + if (existingLineItem) { + await this.prisma.acc_vendor_credit_lines.update({ + where: { + id_acc_vendor_credit_line: + existingLineItem.id_acc_vendor_credit_line, + }, + data: lineItemData, + }); + } else { + await this.prisma.acc_vendor_credit_lines.create({ + data: { + ...lineItemData, + id_acc_vendor_credit_line: uuidv4(), + created_at: new Date(), + }, + }); + } + } + + // Remove any existing line items that are not in the current set + const currentRemoteIds = lineItems.map((item) => item.remote_id); + await this.prisma.acc_vendor_credit_lines.deleteMany({ + where: { + id_acc_vendor_credit: vendorCreditId, + remote_id: { + notIn: currentRemoteIds, + }, + }, + }); } - // Additional methods and logic } diff --git a/packages/api/src/accounting/vendorcredit/types/index.ts b/packages/api/src/accounting/vendorcredit/types/index.ts index 6bd22a4e2..529526306 100644 --- a/packages/api/src/accounting/vendorcredit/types/index.ts +++ b/packages/api/src/accounting/vendorcredit/types/index.ts @@ -5,6 +5,7 @@ import { } from './model.unified'; import { OriginalVendorCreditOutput } from '@@core/utils/types/original/original.accounting'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IVendorCreditService { addVendorCredit( @@ -12,10 +13,7 @@ export interface IVendorCreditService { linkedUserId: string, ): Promise>; - syncVendorCredits( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IVendorCreditMapper { @@ -34,5 +32,7 @@ export interface IVendorCreditMapper { slug: string; remote_id: string; }[], - ): Promise; + ): Promise< + UnifiedAccountingVendorcreditOutput | UnifiedAccountingVendorcreditOutput[] + >; } diff --git a/packages/api/src/accounting/vendorcredit/types/model.unified.ts b/packages/api/src/accounting/vendorcredit/types/model.unified.ts index bef9c0d44..980918563 100644 --- a/packages/api/src/accounting/vendorcredit/types/model.unified.ts +++ b/packages/api/src/accounting/vendorcredit/types/model.unified.ts @@ -1,3 +1,291 @@ -export class UnifiedAccountingVendorcreditInput {} +import { CurrencyCode } from '@@core/utils/types'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsNumber, + IsDateString, + IsArray, +} from 'class-validator'; -export class UnifiedAccountingVendorcreditOutput extends UnifiedAccountingVendorcreditInput {} +export class LineItem { + @ApiPropertyOptional({ + type: String, + example: '100', + nullable: true, + description: 'The net amount of the line item', + }) + @IsString() + @IsOptional() + net_amount?: string; + + @ApiPropertyOptional({ + type: [String], + example: ['801f9ede-c698-4e66-a7fc-48d19eebaa4f'], + nullable: true, + description: + 'The UUIDs of the tracking categories associated with the line item', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tracking_categories?: string[]; + + @ApiPropertyOptional({ + type: String, + example: 'Office supplies', + nullable: true, + description: 'Description of the line item', + }) + @IsString() + @IsOptional() + description?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated account', + }) + @IsUUID() + @IsOptional() + account_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '1.0', + nullable: true, + description: 'The exchange rate for the line item', + }) + @IsString() + @IsOptional() + exchange_rate?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company info', + }) + @IsUUID() + @IsOptional() + company_info_id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'remote_line_item_id_1234', + nullable: true, + description: 'The remote ID of the line item', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + description: 'The created date of the line item', + }) + @IsDateString() + created_at: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + description: 'The last modified date of the line item', + }) + @IsDateString() + modified_at: Date; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated vendor credit', + }) + @IsUUID() + @IsOptional() + vendor_credit_id?: string; +} + +export class UnifiedAccountingVendorcreditInput { + @ApiPropertyOptional({ + type: String, + example: 'VC-001', + nullable: true, + description: 'The number of the vendor credit', + }) + @IsString() + @IsOptional() + number?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The date of the transaction', + }) + @IsDateString() + @IsOptional() + transaction_date?: Date; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the vendor associated with the credit', + }) + @IsUUID() + @IsOptional() + vendor?: string; + + @ApiPropertyOptional({ + type: String, + example: '1000', + nullable: true, + description: 'The total amount of the vendor credit', + }) + @IsNumber() + @IsOptional() + total_amount?: number; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + nullable: true, + enum: CurrencyCode, + description: 'The currency of the vendor credit', + }) + @IsString() + @IsOptional() + currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: String, + example: '1.2', + nullable: true, + description: 'The exchange rate applied to the vendor credit', + }) + @IsString() + @IsOptional() + exchange_rate?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company', + }) + @IsUUID() + @IsOptional() + company_id?: string; + + @ApiPropertyOptional({ + type: [String], + example: ['801f9ede-c698-4e66-a7fc-48d19eebaa4f'], + nullable: true, + description: + 'The UUID of the tracking categories associated with the vendor credit', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + tracking_categories?: string[]; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated accounting period', + }) + @IsUUID() + @IsOptional() + accounting_period_id?: string; + + @ApiPropertyOptional({ + type: [LineItem], + description: 'The line items associated with this vendor credit', + }) + @IsArray() + @IsOptional() + line_items?: LineItem[]; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedAccountingVendorcreditOutput extends UnifiedAccountingVendorcreditInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the vendor credit record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'remote_id_1234', + nullable: true, + description: 'The remote ID of the vendor credit', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: false, + description: 'The created date of the vendor credit', + }) + @IsDateString() + created_at: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: false, + description: 'The last modified date of the vendor credit', + }) + @IsDateString() + modified_at: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: + 'The date when the vendor credit was last updated in the remote system', + }) + @IsDateString() + @IsOptional() + remote_updated_at?: Date; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the vendor credit in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; +} diff --git a/packages/api/src/accounting/vendorcredit/vendorcredit.controller.ts b/packages/api/src/accounting/vendorcredit/vendorcredit.controller.ts index 82ff84220..42d68d6c4 100644 --- a/packages/api/src/accounting/vendorcredit/vendorcredit.controller.ts +++ b/packages/api/src/accounting/vendorcredit/vendorcredit.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -28,8 +30,10 @@ import { import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; import { QueryDto } from '@@core/utils/dtos/query.dto'; -import { ApiGetCustomResponse, ApiPaginatedResponse } from '@@core/utils/dtos/openapi.respone.dto'; - +import { + ApiGetCustomResponse, + ApiPaginatedResponse, +} from '@@core/utils/dtos/openapi.respone.dto'; @ApiTags('accounting/vendorcredits') @Controller('accounting/vendorcredits') @@ -54,6 +58,7 @@ export class VendorCreditController { }) @ApiPaginatedResponse(UnifiedAccountingVendorcreditOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getVendorCredits( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/crm/company/services/company.service.ts b/packages/api/src/crm/company/services/company.service.ts index e7fe42ea2..675822c32 100644 --- a/packages/api/src/crm/company/services/company.service.ts +++ b/packages/api/src/crm/company/services/company.service.ts @@ -526,7 +526,7 @@ export class CompanyService { // Convert the map to an array of objects // Convert the map to an object -const field_mappings = Object.fromEntries(fieldMappingsMap); + const field_mappings = Object.fromEntries(fieldMappingsMap); // Transform to UnifiedCrmCompanyOutput format return { diff --git a/packages/api/src/crm/company/sync/sync.service.ts b/packages/api/src/crm/company/sync/sync.service.ts index 7a4cb0a20..189e10d6f 100644 --- a/packages/api/src/crm/company/sync/sync.service.ts +++ b/packages/api/src/crm/company/sync/sync.service.ts @@ -53,12 +53,12 @@ export class SyncService implements OnModuleInit, IBaseSync { this.logger.log(`Syncing companies....`); const users = user_id ? [ - await this.prisma.users.findUnique({ - where: { - id_user: 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) { @@ -108,7 +108,9 @@ export class SyncService implements OnModuleInit, IBaseSync { const service: ICompanyService = this.serviceRegistry.getService(integrationId); if (!service) { - this.logger.log(`No service found in {vertical:crm, commonObject: company} for integration ID: ${integrationId}`); + this.logger.log( + `No service found in {vertical:crm, commonObject: company} for integration ID: ${integrationId}`, + ); return; } diff --git a/packages/api/src/ecommerce/customer/customer.module.ts b/packages/api/src/ecommerce/customer/customer.module.ts index 6abeee12b..a09f9f380 100644 --- a/packages/api/src/ecommerce/customer/customer.module.ts +++ b/packages/api/src/ecommerce/customer/customer.module.ts @@ -13,7 +13,6 @@ import { WoocommerceCustomerMapper } from './services/woocommerce/mappers'; import { SyncService } from './sync/sync.service'; import { SquarespaceCustomerMapper } from './services/squarespace/mappers'; import { AmazonCustomerMapper } from './services/amazon/mappers'; - @Module({ controllers: [CustomerController], providers: [ diff --git a/packages/api/src/ecommerce/order/services/order.service.ts b/packages/api/src/ecommerce/order/services/order.service.ts index 300b55a99..34161594e 100644 --- a/packages/api/src/ecommerce/order/services/order.service.ts +++ b/packages/api/src/ecommerce/order/services/order.service.ts @@ -2,7 +2,10 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { UnifiedEcommerceOrderInput, UnifiedEcommerceOrderOutput } from '../types/model.unified'; +import { + UnifiedEcommerceOrderInput, + UnifiedEcommerceOrderOutput, +} from '../types/model.unified'; import { OriginalOrderOutput } from '@@core/utils/types/original/original.ecommerce'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; @@ -46,7 +49,7 @@ export class OrderService { } async addOrder( - UnifiedEcommerceOrderData: UnifiedEcommerceOrderInput, + unifiedEcommerceOrderData: UnifiedEcommerceOrderInput, connection_id: string, project_id: string, integrationId: string, @@ -55,11 +58,11 @@ export class OrderService { ): Promise { try { const linkedUser = await this.validateLinkedUser(linkedUserId); - await this.validateCustomerId(UnifiedEcommerceOrderData.customer_id); + await this.validateCustomerId(unifiedEcommerceOrderData.customer_id); const desunifiedObject = await this.coreUnification.desunify({ - sourceObject: UnifiedEcommerceOrderData, + sourceObject: unifiedEcommerceOrderData, targetType: EcommerceObject.order, providerName: integrationId, vertical: 'ecommerce', @@ -328,66 +331,68 @@ export class OrderService { prev_cursor = Buffer.from(cursor).toString('base64'); } - const UnifiedEcommerceOrders: UnifiedEcommerceOrderOutput[] = await Promise.all( - orders.map(async (order) => { - // Fetch field mappings for the order - const values = await this.prisma.value.findMany({ - where: { - entity: { - ressource_owner_id: order.id_ecom_order, + const UnifiedEcommerceOrders: UnifiedEcommerceOrderOutput[] = + await Promise.all( + orders.map(async (order) => { + // Fetch field mappings for the order + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: order.id_ecom_order, + }, }, - }, - include: { - attribute: true, - }, - }); - - // Create a map to store unique field mappings - const fieldMappingsMap = new Map(); - - values.forEach((value) => { - fieldMappingsMap.set(value.attribute.slug, value.data); - }); - - // Convert the map to an array of objects - const field_mappings = Object.fromEntries(fieldMappingsMap); - - // Transform to UnifiedEcommerceOrderOutput format - return { - id: order.id_ecom_order, - order_status: order.order_status, - order_number: order.order_number, - payment_status: order.payment_status, - currency: order.currency as CurrencyCode, - total_price: Number(order.total_price), - total_discount: Number(order.total_discount), - total_shipping: Number(order.total_shipping), - total_tax: Number(order.total_tax), - fulfillment_status: order.fulfillment_status, - customer_id: order.id_ecom_customer, - field_mappings: field_mappings, - remote_id: order.remote_id, - created_at: order.created_at.toISOString(), - modified_at: order.modified_at.toISOString(), - }; - }), - ); + include: { + attribute: true, + }, + }); - let res: UnifiedEcommerceOrderOutput[] = UnifiedEcommerceOrders; + // Create a map to store unique field mappings + const fieldMappingsMap = new Map(); - if (remote_data) { - const remote_array_data: UnifiedEcommerceOrderOutput[] = await Promise.all( - res.map(async (order) => { - const resp = await this.prisma.remote_data.findFirst({ - where: { - ressource_owner_id: order.id, - }, + values.forEach((value) => { + fieldMappingsMap.set(value.attribute.slug, value.data); }); - const remote_data = JSON.parse(resp.data); - return { ...order, remote_data }; + + // Convert the map to an array of objects + const field_mappings = Object.fromEntries(fieldMappingsMap); + + // Transform to UnifiedEcommerceOrderOutput format + return { + id: order.id_ecom_order, + order_status: order.order_status, + order_number: order.order_number, + payment_status: order.payment_status, + currency: order.currency as CurrencyCode, + total_price: Number(order.total_price), + total_discount: Number(order.total_discount), + total_shipping: Number(order.total_shipping), + total_tax: Number(order.total_tax), + fulfillment_status: order.fulfillment_status, + customer_id: order.id_ecom_customer, + field_mappings: field_mappings, + remote_id: order.remote_id, + created_at: order.created_at.toISOString(), + modified_at: order.modified_at.toISOString(), + }; }), ); + let res: UnifiedEcommerceOrderOutput[] = UnifiedEcommerceOrders; + + if (remote_data) { + const remote_array_data: UnifiedEcommerceOrderOutput[] = + await Promise.all( + res.map(async (order) => { + const resp = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: order.id, + }, + }); + const remote_data = JSON.parse(resp.data); + return { ...order, remote_data }; + }), + ); + res = remote_array_data; } diff --git a/packages/api/src/hris/@lib/@types/index.ts b/packages/api/src/hris/@lib/@types/index.ts index 8ca678053..152fe0763 100644 --- a/packages/api/src/hris/@lib/@types/index.ts +++ b/packages/api/src/hris/@lib/@types/index.ts @@ -67,6 +67,11 @@ import { UnifiedHrisTimeoffbalanceInput, UnifiedHrisTimeoffbalanceOutput, } from '@hris/timeoffbalance/types/model.unified'; +import { ITimesheetentryService } from '@hris/timesheetentry/types'; +import { + UnifiedHrisTimesheetEntryInput, + UnifiedHrisTimesheetEntryOutput, +} from '@hris/timesheetentry/types/model.unified'; export enum HrisObject { bankinfo = 'bankinfo', @@ -83,6 +88,7 @@ export enum HrisObject { payrollrun = 'payrollrun', timeoff = 'timeoff', timeoffbalance = 'timeoffbalance', + timesheetentry = 'timesheetentry', } export type UnifiedHris = @@ -113,7 +119,9 @@ export type UnifiedHris = | UnifiedHrisLocationInput | UnifiedHrisLocationOutput | UnifiedHrisPaygroupInput - | UnifiedHrisPaygroupOutput; + | UnifiedHrisPaygroupOutput + | UnifiedHrisTimesheetEntryInput + | UnifiedHrisTimesheetEntryOutput; export type IHrisService = | IBankInfoService @@ -128,4 +136,5 @@ export type IHrisService = | ITimeoffBalanceService | IPayrollRunService | IPayGroupService - | ILocationService; + | ILocationService + | ITimesheetentryService; diff --git a/packages/api/src/hris/@lib/@utils/index.ts b/packages/api/src/hris/@lib/@utils/index.ts new file mode 100644 index 000000000..23b09c261 --- /dev/null +++ b/packages/api/src/hris/@lib/@utils/index.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; + +@Injectable() +export class Utils { + constructor(private readonly prisma: PrismaService) {} + + async getEmployeeUuidFromRemoteId(id: string, connection_id: string) { + try { + const res = await this.prisma.hris_employees.findFirst({ + where: { + remote_id: id, + id_connection: connection_id, + }, + }); + if (!res) return; + return res.id_hris_employee; + } catch (error) { + throw error; + } + } + + async getCompanyUuidFromRemoteId(id: string, connection_id: string) { + try { + const res = await this.prisma.hris_companies.findFirst({ + where: { + remote_id: id, + id_connection: connection_id, + }, + }); + if (!res) return; + return res.id_hris_company; + } catch (error) { + throw error; + } + } + + async getEmployerBenefitUuidFromRemoteId(id: string, connection_id: string) { + try { + const res = await this.prisma.hris_employer_benefits.findFirst({ + where: { + remote_id: id, + id_connection: connection_id, + }, + }); + if (!res) return; + return res.id_hris_employer_benefit; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/hris/bankinfo/bankinfo.controller.ts b/packages/api/src/hris/bankinfo/bankinfo.controller.ts index d97f772a6..b1c2a6db7 100644 --- a/packages/api/src/hris/bankinfo/bankinfo.controller.ts +++ b/packages/api/src/hris/bankinfo/bankinfo.controller.ts @@ -7,6 +7,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -32,7 +34,6 @@ import { ApiPaginatedResponse, } from '@@core/utils/dtos/openapi.respone.dto'; - @ApiTags('hris/bankinfos') @Controller('hris/bankinfos') export class BankinfoController { @@ -107,6 +108,7 @@ export class BankinfoController { example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) @ApiGetCustomResponse(UnifiedHrisBankinfoOutput) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @UseGuards(ApiKeyAuthGuard) @Get(':id') async retrieve( diff --git a/packages/api/src/hris/bankinfo/services/bankinfo.service.ts b/packages/api/src/hris/bankinfo/services/bankinfo.service.ts index 0c6ff3879..87de1f944 100644 --- a/packages/api/src/hris/bankinfo/services/bankinfo.service.ts +++ b/packages/api/src/hris/bankinfo/services/bankinfo.service.ts @@ -3,9 +3,9 @@ import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { Injectable } from '@nestjs/common'; import { UnifiedHrisBankinfoOutput } from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; +import { v4 as uuidv4 } from 'uuid'; @Injectable() export class BankInfoService { @@ -20,14 +20,79 @@ export class BankInfoService { } async getBankinfo( - id_bankinfoing_bankinfo: string, + id_hris_bank_info: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const bankInfo = await this.prisma.hris_bank_infos.findUnique({ + where: { id_hris_bank_info: id_hris_bank_info }, + }); + + if (!bankInfo) { + throw new Error(`Bank info with ID ${id_hris_bank_info} not found.`); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: bankInfo.id_hris_bank_info }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedBankInfo: UnifiedHrisBankinfoOutput = { + id: bankInfo.id_hris_bank_info, + account_type: bankInfo.account_type, + bank_name: bankInfo.bank_name, + account_number: bankInfo.account_number, + routing_number: bankInfo.routing_number, + employee_id: bankInfo.id_hris_employee, + field_mappings: field_mappings, + remote_id: bankInfo.remote_id, + remote_created_at: bankInfo.remote_created_at, + created_at: bankInfo.created_at, + modified_at: bankInfo.modified_at, + remote_was_deleted: bankInfo.remote_was_deleted, + }; + + const res: UnifiedHrisBankinfoOutput = unifiedBankInfo; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: bankInfo.id_hris_bank_info }, + }); + res.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.bankinfo.pull', + method: 'GET', + url: '/hris/bankinfo', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return res; + } catch (error) { + throw error; + } } async getBankinfos( @@ -38,7 +103,88 @@ export class BankInfoService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedHrisBankinfoOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const bankInfos = await this.prisma.hris_bank_infos.findMany({ + take: limit + 1, + cursor: cursor ? { id_hris_bank_info: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = bankInfos.length > limit; + if (hasNextPage) bankInfos.pop(); + + const unifiedBankInfos = await Promise.all( + bankInfos.map(async (bankInfo) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: bankInfo.id_hris_bank_info }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedBankInfo: UnifiedHrisBankinfoOutput = { + id: bankInfo.id_hris_bank_info, + account_type: bankInfo.account_type, + bank_name: bankInfo.bank_name, + account_number: bankInfo.account_number, + routing_number: bankInfo.routing_number, + employee_id: bankInfo.id_hris_employee, + field_mappings: field_mappings, + remote_id: bankInfo.remote_id, + remote_created_at: bankInfo.remote_created_at, + created_at: bankInfo.created_at, + modified_at: bankInfo.modified_at, + remote_was_deleted: bankInfo.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: bankInfo.id_hris_bank_info }, + }); + unifiedBankInfo.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedBankInfo; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.bankinfo.pull', + method: 'GET', + url: '/hris/bankinfos', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedBankInfos, + next_cursor: hasNextPage + ? bankInfos[bankInfos.length - 1].id_hris_bank_info + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/hris/bankinfo/sync/sync.processor.ts b/packages/api/src/hris/bankinfo/sync/sync.processor.ts new file mode 100644 index 000000000..b61deb815 --- /dev/null +++ b/packages/api/src/hris/bankinfo/sync/sync.processor.ts @@ -0,0 +1,19 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; +import { Queues } from '@@core/@core-services/queues/types'; + +@Processor(Queues.SYNC_JOBS_WORKER) +export class SyncProcessor { + constructor(private syncService: SyncService) {} + + @Process('hris-sync-bankinfos') + async handleSyncCustomers(job: Job) { + try { + console.log(`Processing queue -> hris-sync-bankinfos ${job.id}`); + await this.syncService.kickstartSync(); + } catch (error) { + console.error('Error syncing hris bank infos', error); + } + } +} diff --git a/packages/api/src/hris/bankinfo/sync/sync.service.ts b/packages/api/src/hris/bankinfo/sync/sync.service.ts index fd5017577..df1aa1f6c 100644 --- a/packages/api/src/hris/bankinfo/sync/sync.service.ts +++ b/packages/api/src/hris/bankinfo/sync/sync.service.ts @@ -2,12 +2,19 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { UnifiedHrisBankinfoOutput } from '../types/model.unified'; +import { HRIS_PROVIDERS } from '@panora/shared'; +import { hris_bank_infos as HrisBankInfo } from '@prisma/client'; +import { OriginalBankInfoOutput } from '@@core/utils/types/original/original.hris'; +import { IBankInfoService } from '../types'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -17,23 +24,139 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('hris', 'bankinfo', this); } - saveToDb( + + async onModuleInit() { + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing bank infos...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of HRIS_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IBankInfoService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedHrisBankinfoOutput, + OriginalBankInfoOutput, + IBankInfoService + >(integrationId, linkedUserId, 'hris', 'bankinfo', service, []); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + bankInfos: UnifiedHrisBankinfoOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const bankInfoResults: HrisBankInfo[] = []; - async onModuleInit() { - // Initialization logic - } + for (let i = 0; i < bankInfos.length; i++) { + const bankInfo = bankInfos[i]; + const originId = bankInfo.remote_id; + + let existingBankInfo = await this.prisma.hris_bank_infos.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const bankInfoData = { + account_type: bankInfo.account_type, + bank_name: bankInfo.bank_name, + account_number: bankInfo.account_number, + routing_number: bankInfo.routing_number, + id_hris_employee: bankInfo.employee_id, + remote_id: originId, + remote_created_at: bankInfo.remote_created_at + ? new Date(bankInfo.remote_created_at) + : null, + modified_at: new Date(), + remote_was_deleted: bankInfo.remote_was_deleted, + }; - // Additional methods and logic + if (existingBankInfo) { + existingBankInfo = await this.prisma.hris_bank_infos.update({ + where: { id_hris_bank_info: existingBankInfo.id_hris_bank_info }, + data: bankInfoData, + }); + } else { + existingBankInfo = await this.prisma.hris_bank_infos.create({ + data: { + ...bankInfoData, + id_hris_bank_info: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + bankInfoResults.push(existingBankInfo); + + // Process field mappings + await this.ingestService.processFieldMappings( + bankInfo.field_mappings, + existingBankInfo.id_hris_bank_info, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingBankInfo.id_hris_bank_info, + remote_data[i], + ); + } + + return bankInfoResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/hris/bankinfo/types/index.ts b/packages/api/src/hris/bankinfo/types/index.ts index 0433850ea..c1eb22cf9 100644 --- a/packages/api/src/hris/bankinfo/types/index.ts +++ b/packages/api/src/hris/bankinfo/types/index.ts @@ -1,18 +1,14 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { UnifiedHrisBankinfoInput, UnifiedHrisBankinfoOutput } from './model.unified'; +import { + UnifiedHrisBankinfoInput, + UnifiedHrisBankinfoOutput, +} from './model.unified'; import { OriginalBankInfoOutput } from '@@core/utils/types/original/original.hris'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IBankInfoService { - addBankinfo( - bankinfoData: DesunifyReturnType, - linkedUserId: string, - ): Promise>; - - syncBankinfos( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IBankinfoMapper { diff --git a/packages/api/src/hris/bankinfo/types/model.unified.ts b/packages/api/src/hris/bankinfo/types/model.unified.ts index a101d7dc1..e3a7cbe3b 100644 --- a/packages/api/src/hris/bankinfo/types/model.unified.ts +++ b/packages/api/src/hris/bankinfo/types/model.unified.ts @@ -1,3 +1,150 @@ -export class UnifiedHrisBankinfoInput {} +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsDateString, + IsBoolean, +} from 'class-validator'; -export class UnifiedHrisBankinfoOutput extends UnifiedHrisBankinfoInput {} +export type AccountType = 'SAVINGS' | 'CHECKING'; + +export class UnifiedHrisBankinfoInput { + @ApiPropertyOptional({ + type: String, + example: 'CHECKING', + enum: ['SAVINGS', 'CHECKING'], + nullable: true, + description: 'The type of the bank account', + }) + @IsString() + @IsOptional() + account_type?: AccountType | string; + + @ApiPropertyOptional({ + type: String, + example: 'Bank of America', + nullable: true, + description: 'The name of the bank', + }) + @IsString() + @IsOptional() + bank_name?: string; + + @ApiPropertyOptional({ + type: String, + example: '1234567890', + nullable: true, + description: 'The account number', + }) + @IsString() + @IsOptional() + account_number?: string; + + @ApiPropertyOptional({ + type: String, + example: '021000021', + nullable: true, + description: 'The routing number of the bank', + }) + @IsString() + @IsOptional() + routing_number?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated employee', + }) + @IsUUID() + @IsOptional() + employee_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedHrisBankinfoOutput extends UnifiedHrisBankinfoInput { + @ApiProperty({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the bank info record', + }) + @IsUUID() + id: string; + + @ApiPropertyOptional({ + type: String, + example: 'id_1', + nullable: true, + description: + 'The remote ID of the bank info in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the bank info in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: + 'The date when the bank info was created in the 3rd party system', + }) + @IsDateString() + @IsOptional() + remote_created_at: Date; + + @ApiProperty({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The created date of the bank info record', + }) + @IsDateString() + created_at: Date; + + @ApiProperty({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The last modified date of the bank info record', + }) + @IsDateString() + modified_at: Date; + + @ApiProperty({ + type: Boolean, + example: false, + nullable: true, + description: 'Indicates if the bank info was deleted in the remote system', + }) + @IsBoolean() + remote_was_deleted: boolean; +} diff --git a/packages/api/src/hris/benefit/benefit.controller.ts b/packages/api/src/hris/benefit/benefit.controller.ts index cfdaf0aa5..b19e25efc 100644 --- a/packages/api/src/hris/benefit/benefit.controller.ts +++ b/packages/api/src/hris/benefit/benefit.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -33,7 +35,6 @@ import { ApiPaginatedResponse, } from '@@core/utils/dtos/openapi.respone.dto'; - @ApiTags('hris/benefits') @Controller('hris/benefits') export class BenefitController { @@ -56,6 +57,7 @@ export class BenefitController { example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) @ApiPaginatedResponse(UnifiedHrisBenefitOutput) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @UseGuards(ApiKeyAuthGuard) @Get() async getBenefits( diff --git a/packages/api/src/hris/benefit/benefit.module.ts b/packages/api/src/hris/benefit/benefit.module.ts index 181f4f8b4..30ef672e5 100644 --- a/packages/api/src/hris/benefit/benefit.module.ts +++ b/packages/api/src/hris/benefit/benefit.module.ts @@ -1,35 +1,28 @@ import { Module } from '@nestjs/common'; import { BenefitController } from './benefit.controller'; -import { SyncService } from './sync/sync.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { BenefitService } from './services/benefit.service'; import { ServiceRegistry } from './services/registry.service'; -import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; - -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { BullModule } from '@nestjs/bull'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { SyncService } from './sync/sync.service'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; - +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { GustoService } from './services/gusto'; +import { GustoBenefitMapper } from './services/gusto/mappers'; +import { Utils } from '@hris/@lib/@utils'; @Module({ controllers: [BenefitController], providers: [ BenefitService, - SyncService, WebhookService, - ServiceRegistry, - IngestDataService, CoreUnification, - + Utils, + GustoBenefitMapper, /* PROVIDERS SERVICES */ + GustoService, ], exports: [SyncService], }) diff --git a/packages/api/src/hris/benefit/services/benefit.service.ts b/packages/api/src/hris/benefit/services/benefit.service.ts index 786985bd5..f98e3422f 100644 --- a/packages/api/src/hris/benefit/services/benefit.service.ts +++ b/packages/api/src/hris/benefit/services/benefit.service.ts @@ -1,20 +1,11 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { - UnifiedHrisBenefitInput, - UnifiedHrisBenefitOutput, -} from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; +import { UnifiedHrisBenefitOutput } from '../types/model.unified'; import { ServiceRegistry } from './registry.service'; -import { OriginalBenefitOutput } from '@@core/utils/types/original/original.hris'; - -import { IBenefitService } from '../types'; @Injectable() export class BenefitService { @@ -29,14 +20,79 @@ export class BenefitService { } async getBenefit( - id_benefiting_benefit: string, + id_hris_benefit: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const benefit = await this.prisma.hris_benefits.findUnique({ + where: { id_hris_benefit: id_hris_benefit }, + }); + + if (!benefit) { + throw new Error(`Benefit with ID ${id_hris_benefit} not found.`); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: benefit.id_hris_benefit }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedBenefit: UnifiedHrisBenefitOutput = { + id: benefit.id_hris_benefit, + provider_name: benefit.provider_name, + employee_id: benefit.id_hris_employee, + employee_contribution: Number(benefit.employee_contribution), + company_contribution: Number(benefit.company_contribution), + start_date: benefit.start_date, + end_date: benefit.end_date, + employer_benefit_id: benefit.id_hris_employer_benefit, + field_mappings: field_mappings, + remote_id: benefit.remote_id, + remote_created_at: benefit.remote_created_at, + created_at: benefit.created_at, + modified_at: benefit.modified_at, + remote_was_deleted: benefit.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: benefit.id_hris_benefit }, + }); + unifiedBenefit.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.benefit.pull', + method: 'GET', + url: '/hris/benefit', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedBenefit; + } catch (error) { + throw error; + } } async getBenefits( @@ -47,7 +103,90 @@ export class BenefitService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedHrisBenefitOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const benefits = await this.prisma.hris_benefits.findMany({ + take: limit + 1, + cursor: cursor ? { id_hris_benefit: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = benefits.length > limit; + if (hasNextPage) benefits.pop(); + + const unifiedBenefits = await Promise.all( + benefits.map(async (benefit) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: benefit.id_hris_benefit }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedBenefit: UnifiedHrisBenefitOutput = { + id: benefit.id_hris_benefit, + provider_name: benefit.provider_name, + employee_id: benefit.id_hris_employee, + employee_contribution: Number(benefit.employee_contribution), + company_contribution: Number(benefit.company_contribution), + start_date: benefit.start_date, + end_date: benefit.end_date, + employer_benefit_id: benefit.id_hris_employer_benefit, + field_mappings: field_mappings, + remote_id: benefit.remote_id, + remote_created_at: benefit.remote_created_at, + created_at: benefit.created_at, + modified_at: benefit.modified_at, + remote_was_deleted: benefit.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: benefit.id_hris_benefit }, + }); + unifiedBenefit.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedBenefit; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.benefit.pull', + method: 'GET', + url: '/hris/benefits', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedBenefits, + next_cursor: hasNextPage + ? benefits[benefits.length - 1].id_hris_benefit + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/hris/benefit/services/gusto/index.ts b/packages/api/src/hris/benefit/services/gusto/index.ts new file mode 100644 index 000000000..7686d0d78 --- /dev/null +++ b/packages/api/src/hris/benefit/services/gusto/index.ts @@ -0,0 +1,72 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { HrisObject } from '@hris/@lib/@types'; +import { IBenefitService } from '@hris/benefit/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { GustoBenefitOutput } from './types'; + +@Injectable() +export class GustoService implements IBenefitService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + HrisObject.benefit.toUpperCase() + ':' + GustoService.name, + ); + this.registry.registerService('gusto', this); + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId, id_employee } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'gusto', + vertical: 'hris', + }, + }); + + const employee = await this.prisma.hris_employees.findUnique({ + where: { + id_hris_employee: id_employee as string, + }, + select: { + remote_id: true, + }, + }); + + const resp = await axios.get( + `${connection.account_url}/v1/employees/${employee.remote_id}/employee_benefits`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced gusto benefits !`); + + return { + data: resp.data, + message: 'Gusto benefits retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/hris/benefit/services/gusto/mappers.ts b/packages/api/src/hris/benefit/services/gusto/mappers.ts new file mode 100644 index 000000000..6552a5fc5 --- /dev/null +++ b/packages/api/src/hris/benefit/services/gusto/mappers.ts @@ -0,0 +1,93 @@ +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { Injectable } from '@nestjs/common'; +import { GustoBenefitOutput } from './types'; +import { + UnifiedHrisBenefitInput, + UnifiedHrisBenefitOutput, +} from '@hris/benefit/types/model.unified'; +import { IBenefitMapper } from '@hris/benefit/types'; +import { Utils } from '@hris/@lib/@utils'; + +@Injectable() +export class GustoBenefitMapper implements IBenefitMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService('hris', 'benefit', 'gusto', this); + } + + async desunify( + source: UnifiedHrisBenefitInput, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return; + } + + async unify( + source: GustoBenefitOutput | GustoBenefitOutput[], + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleBenefitToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((benefit) => + this.mapSingleBenefitToUnified( + benefit, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleBenefitToUnified( + benefit: GustoBenefitOutput, + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + const opts: any = {}; + + if (benefit.employee_uuid) { + const employee_id = await this.utils.getEmployeeUuidFromRemoteId( + benefit.employee_uuid, + connectionId, + ); + if (employee_id) { + opts.employee_id = employee_id; + } + } + if (benefit.company_benefit_uuid) { + const id = await this.utils.getEmployerBenefitUuidFromRemoteId( + benefit.company_benefit_uuid, + connectionId, + ); + if (id) { + opts.employer_benefit_id = id; + } + } + + return { + remote_id: benefit.uuid || null, + remote_data: benefit, + ...opts, + employee_contribution: benefit.employee_deduction + ? parseFloat(benefit.employee_deduction) + : null, + company_contribution: benefit.company_contribution + ? parseFloat(benefit.company_contribution) + : null, + remote_was_deleted: null, + }; + } +} diff --git a/packages/api/src/hris/benefit/services/gusto/types.ts b/packages/api/src/hris/benefit/services/gusto/types.ts new file mode 100644 index 000000000..1f923b5a6 --- /dev/null +++ b/packages/api/src/hris/benefit/services/gusto/types.ts @@ -0,0 +1,28 @@ +export type GustoBenefitOutput = Partial<{ + version: string; + employee_uuid: string; + company_benefit_uuid: string; + active: boolean; + uuid: string; + employee_deduction: string; + company_contribution: string; + employee_deduction_annual_maximum: string; + company_contribution_annual_maximum: string; + limit_option: string; + deduct_as_percentage: boolean; + contribute_as_percentage: boolean; + catch_up: boolean; + coverage_amount: string; + contribution: { + type: 'amount' | 'percentage' | 'tiered'; + value: + | string + | number + | Array<{ threshold: number; amount: string | number }>; + }; + deduction_reduces_taxable_income: + | 'unset' + | 'reduces_taxable_income' + | 'does_not_reduce_taxable_income'; + coverage_salary_multiplier: string; +}>; diff --git a/packages/api/src/hris/benefit/sync/sync.processor.ts b/packages/api/src/hris/benefit/sync/sync.processor.ts new file mode 100644 index 000000000..920612a27 --- /dev/null +++ b/packages/api/src/hris/benefit/sync/sync.processor.ts @@ -0,0 +1,19 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; +import { Queues } from '@@core/@core-services/queues/types'; + +@Processor(Queues.SYNC_JOBS_WORKER) +export class SyncProcessor { + constructor(private syncService: SyncService) {} + + @Process('hris-sync-benefits') + async handleSyncBenefits(job: Job) { + try { + console.log(`Processing queue -> hris-sync-benefits ${job.id}`); + await this.syncService.kickstartSync(); + } catch (error) { + console.error('Error syncing hris benefits', error); + } + } +} diff --git a/packages/api/src/hris/benefit/sync/sync.service.ts b/packages/api/src/hris/benefit/sync/sync.service.ts index 507b88c3e..054265fd2 100644 --- a/packages/api/src/hris/benefit/sync/sync.service.ts +++ b/packages/api/src/hris/benefit/sync/sync.service.ts @@ -1,15 +1,20 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { OriginalBenefitOutput } from '@@core/utils/types/original/original.hris'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; +import { HRIS_PROVIDERS } from '@panora/shared'; +import { hris_benefits as HrisBenefit } from '@prisma/client'; import { v4 as uuidv4 } from 'uuid'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { UnifiedHrisBenefitOutput } from '../types/model.unified'; import { IBenefitService } from '../types'; -import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { UnifiedHrisBenefitOutput } from '../types/model.unified'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -19,23 +24,152 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('hris', 'benefit', this); + } + + async onModuleInit() { + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing benefits...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of HRIS_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId, id_employee } = param; + const service: IBenefitService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedHrisBenefitOutput, + OriginalBenefitOutput, + IBenefitService + >(integrationId, linkedUserId, 'hris', 'benefit', service, [ + { + param: id_employee, + paramName: 'id_employee', + shouldPassToService: true, + shouldPassToIngest: true, + }, + ]); + } catch (error) { + throw error; + } } - saveToDb( + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + benefits: UnifiedHrisBenefitOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const benefitResults: HrisBenefit[] = []; - async onModuleInit() { - // Initialization logic - } + for (let i = 0; i < benefits.length; i++) { + const benefit = benefits[i]; + const originId = benefit.remote_id; - // Additional methods and logic + let existingBenefit = await this.prisma.hris_benefits.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const benefitData = { + provider_name: benefit.provider_name, + id_hris_employee: benefit.employee_id, + employee_contribution: benefit.employee_contribution + ? BigInt(benefit.employee_contribution) + : null, + company_contribution: benefit.company_contribution + ? BigInt(benefit.company_contribution) + : null, + start_date: benefit.start_date ? new Date(benefit.start_date) : null, + end_date: benefit.end_date ? new Date(benefit.end_date) : null, + id_hris_employer_benefit: benefit.employer_benefit_id, + remote_id: originId, + remote_created_at: benefit.remote_created_at + ? new Date(benefit.remote_created_at) + : null, + modified_at: new Date(), + remote_was_deleted: benefit.remote_was_deleted || false, + }; + + if (existingBenefit) { + existingBenefit = await this.prisma.hris_benefits.update({ + where: { id_hris_benefit: existingBenefit.id_hris_benefit }, + data: benefitData, + }); + } else { + existingBenefit = await this.prisma.hris_benefits.create({ + data: { + ...benefitData, + id_hris_benefit: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + benefitResults.push(existingBenefit); + + // Process field mappings + await this.ingestService.processFieldMappings( + benefit.field_mappings, + existingBenefit.id_hris_benefit, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingBenefit.id_hris_benefit, + remote_data[i], + ); + } + + return benefitResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/hris/benefit/types/index.ts b/packages/api/src/hris/benefit/types/index.ts index d636c676a..ae78e2b9a 100644 --- a/packages/api/src/hris/benefit/types/index.ts +++ b/packages/api/src/hris/benefit/types/index.ts @@ -1,18 +1,14 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { UnifiedHrisBenefitInput, UnifiedHrisBenefitOutput } from './model.unified'; +import { + UnifiedHrisBenefitInput, + UnifiedHrisBenefitOutput, +} from './model.unified'; import { OriginalBenefitOutput } from '@@core/utils/types/original/original.hris'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IBenefitService { - addBenefit( - benefitData: DesunifyReturnType, - linkedUserId: string, - ): Promise>; - - syncBenefits( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IBenefitMapper { diff --git a/packages/api/src/hris/benefit/types/model.unified.ts b/packages/api/src/hris/benefit/types/model.unified.ts index ebe523e39..5c2e16f94 100644 --- a/packages/api/src/hris/benefit/types/model.unified.ts +++ b/packages/api/src/hris/benefit/types/model.unified.ts @@ -1,3 +1,169 @@ -export class UnifiedHrisBenefitInput {} +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsDateString, + IsNumber, +} from 'class-validator'; -export class UnifiedHrisBenefitOutput extends UnifiedHrisBenefitInput {} +export class UnifiedHrisBenefitInput { + @ApiPropertyOptional({ + type: String, + example: 'Health Insurance Provider', + nullable: true, + description: 'The name of the benefit provider', + }) + @IsString() + @IsOptional() + provider_name?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated employee', + }) + @IsUUID() + @IsOptional() + employee_id?: string; + + @ApiPropertyOptional({ + type: Number, + example: 100, + nullable: true, + description: 'The employee contribution amount', + }) + @IsNumber() + @IsOptional() + employee_contribution?: number; + + @ApiPropertyOptional({ + type: Number, + example: 200, + nullable: true, + description: 'The company contribution amount', + }) + @IsNumber() + @IsOptional() + company_contribution?: number; + + @ApiPropertyOptional({ + type: Date, + example: '2024-01-01T00:00:00Z', + nullable: true, + description: 'The start date of the benefit', + }) + @IsDateString() + @IsOptional() + start_date?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-12-31T23:59:59Z', + nullable: true, + description: 'The end date of the benefit', + }) + @IsDateString() + @IsOptional() + end_date?: Date; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated employer benefit', + }) + @IsUUID() + @IsOptional() + employer_benefit_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedHrisBenefitOutput extends UnifiedHrisBenefitInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the benefit record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'benefit_1234', + nullable: true, + description: 'The remote ID of the benefit in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the benefit in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: + 'The date when the benefit was created in the 3rd party system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The created date of the benefit record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The last modified date of the benefit record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; + + @ApiPropertyOptional({ + type: Boolean, + example: false, + nullable: true, + description: 'Indicates if the benefit was deleted in the remote system', + }) + @IsOptional() + remote_was_deleted?: boolean; +} diff --git a/packages/api/src/hris/company/company.controller.ts b/packages/api/src/hris/company/company.controller.ts index 835ff7bd1..842101332 100644 --- a/packages/api/src/hris/company/company.controller.ts +++ b/packages/api/src/hris/company/company.controller.ts @@ -1,36 +1,30 @@ +import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { Controller, - Post, - Body, - Query, Get, - Patch, - Param, Headers, + Param, + Query, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { - ApiBody, + ApiHeader, ApiOperation, ApiParam, ApiQuery, ApiTags, - ApiHeader, - //ApiKeyAuth, } from '@nestjs/swagger'; - -import { UnifiedHrisCompanyOutput } from './types/model.unified'; -import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; -import { CompanyService } from './services/company.service'; -import { QueryDto } from '@@core/utils/dtos/query.dto'; +import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiGetCustomResponse, ApiPaginatedResponse, } from '@@core/utils/dtos/openapi.respone.dto'; -import { query } from 'express'; - +import { QueryDto } from '@@core/utils/dtos/query.dto'; +import { CompanyService } from './services/company.service'; +import { UnifiedHrisCompanyOutput } from './types/model.unified'; @ApiTags('hris/companies') @Controller('hris/companies') @@ -55,6 +49,7 @@ export class CompanyController { }) @ApiPaginatedResponse(UnifiedHrisCompanyOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getCompanies( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/hris/company/company.module.ts b/packages/api/src/hris/company/company.module.ts index bc2b03682..eae8227f4 100644 --- a/packages/api/src/hris/company/company.module.ts +++ b/packages/api/src/hris/company/company.module.ts @@ -1,35 +1,27 @@ import { Module } from '@nestjs/common'; import { CompanyController } from './company.controller'; -import { SyncService } from './sync/sync.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { CompanyService } from './services/company.service'; import { ServiceRegistry } from './services/registry.service'; -import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; - -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { BullModule } from '@nestjs/bull'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { SyncService } from './sync/sync.service'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; - +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; - +import { GustoCompanyMapper } from './services/gusto/mappers'; +import { GustoService } from './services/gusto'; +import { Utils } from '@hris/@lib/@utils'; @Module({ controllers: [CompanyController], providers: [ CompanyService, CoreUnification, - SyncService, - WebhookService, - + Utils, ServiceRegistry, - IngestDataService, + GustoCompanyMapper, /* PROVIDERS SERVICES */ + GustoService, ], exports: [SyncService], }) diff --git a/packages/api/src/hris/company/services/company.service.ts b/packages/api/src/hris/company/services/company.service.ts index 3fdee9a57..9f9e0b4a7 100644 --- a/packages/api/src/hris/company/services/company.service.ts +++ b/packages/api/src/hris/company/services/company.service.ts @@ -1,16 +1,11 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { UnifiedHrisCompanyOutput } from '../types/model.unified'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; +import { UnifiedHrisCompanyOutput } from '../types/model.unified'; import { ServiceRegistry } from './registry.service'; -import { OriginalCompanyOutput } from '@@core/utils/types/original/original.hris'; - -import { ICompanyService } from '../types'; @Injectable() export class CompanyService { @@ -25,14 +20,82 @@ export class CompanyService { } async getCompany( - id_companying_company: string, + id_hris_company: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const company = await this.prisma.hris_companies.findUnique({ + where: { id_hris_company: id_hris_company }, + }); + + if (!company) { + throw new Error(`Company with ID ${id_hris_company} not found.`); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: company.id_hris_company }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const locations = await this.prisma.hris_locations.findMany({ + where: { + id_hris_company: company.id_hris_company, + }, + }); + + const unifiedCompany: UnifiedHrisCompanyOutput = { + id: company.id_hris_company, + legal_name: company.legal_name, + display_name: company.display_name, + eins: company.eins, + field_mappings: field_mappings, + locations: locations.map((loc) => loc.id_hris_location), + remote_id: company.remote_id, + remote_created_at: company.remote_created_at, + created_at: company.created_at, + modified_at: company.modified_at, + remote_was_deleted: company.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: company.id_hris_company }, + }); + unifiedCompany.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.company.pull', + method: 'GET', + url: '/hris/company', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedCompany; + } catch (error) { + throw error; + } } async getCompanies( @@ -43,7 +106,93 @@ export class CompanyService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedHrisCompanyOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const companies = await this.prisma.hris_companies.findMany({ + take: limit + 1, + cursor: cursor ? { id_hris_company: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = companies.length > limit; + if (hasNextPage) companies.pop(); + + const unifiedCompanies = await Promise.all( + companies.map(async (company) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: company.id_hris_company }, + }, + include: { attribute: true }, + }); + + const locations = await this.prisma.hris_locations.findMany({ + where: { + id_hris_company: company.id_hris_company, + }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedCompany: UnifiedHrisCompanyOutput = { + id: company.id_hris_company, + legal_name: company.legal_name, + display_name: company.display_name, + eins: company.eins, + field_mappings: field_mappings, + locations: locations.map((loc) => loc.id_hris_location), + remote_id: company.remote_id, + remote_created_at: company.remote_created_at, + created_at: company.created_at, + modified_at: company.modified_at, + remote_was_deleted: company.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: company.id_hris_company }, + }); + unifiedCompany.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedCompany; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.company.pull', + method: 'GET', + url: '/hris/companies', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedCompanies, + next_cursor: hasNextPage + ? companies[companies.length - 1].id_hris_company + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/hris/company/services/gusto/index.ts b/packages/api/src/hris/company/services/gusto/index.ts new file mode 100644 index 000000000..bfc8bfa99 --- /dev/null +++ b/packages/api/src/hris/company/services/gusto/index.ts @@ -0,0 +1,81 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { HrisObject } from '@hris/@lib/@types'; +import { ICompanyService } from '@hris/company/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { GustoCompanyOutput } from './types'; +import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; +import { OriginalCompanyOutput } from '@@core/utils/types/original/original.hris'; + +@Injectable() +export class GustoService implements ICompanyService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + HrisObject.company.toUpperCase() + ':' + GustoService.name, + ); + this.registry.registerService('gusto', this); + } + + addCompany( + companyData: DesunifyReturnType, + linkedUserId: string, + ): Promise> { + throw new Error('Method not implemented.'); + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'gusto', + vertical: 'hris', + }, + }); + + const resp = await axios.get(`${connection.account_url}/v1/token_info`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + const company_uuid = resp.data.resource.uuid; + const resp_ = await axios.get( + `${connection.account_url}/v1/companies/${company_uuid}`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced gusto companys !`); + + return { + data: [resp_.data], + message: 'Gusto companys retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/hris/company/services/gusto/mappers.ts b/packages/api/src/hris/company/services/gusto/mappers.ts new file mode 100644 index 000000000..91edecfbe --- /dev/null +++ b/packages/api/src/hris/company/services/gusto/mappers.ts @@ -0,0 +1,88 @@ +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { Injectable } from '@nestjs/common'; +import { GustoCompanyOutput } from './types'; +import { + UnifiedHrisCompanyInput, + UnifiedHrisCompanyOutput, +} from '@hris/company/types/model.unified'; +import { ICompanyMapper } from '@hris/company/types'; +import { Utils } from '@hris/@lib/@utils'; +import { UnifiedHrisLocationOutput } from '@hris/location/types/model.unified'; +import { GustoLocationOutput } from '@hris/location/services/gusto/types'; +import { HrisObject } from '@hris/@lib/@types'; + +@Injectable() +export class GustoCompanyMapper implements ICompanyMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService('hris', 'company', 'gusto', this); + } + + async desunify( + source: UnifiedHrisCompanyInput, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return; + } + + async unify( + source: GustoCompanyOutput | GustoCompanyOutput[], + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleCompanyToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((company) => + this.mapSingleCompanyToUnified( + company, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleCompanyToUnified( + company: GustoCompanyOutput, + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + const opts: any = {}; + if (company.locations && company.locations.length > 0) { + const locations = await this.ingestService.ingestData< + UnifiedHrisLocationOutput, + GustoLocationOutput + >( + company.locations, + 'gusto', + connectionId, + 'hris', + HrisObject.location, + [], + ); + if (locations) { + opts.locations = locations.map((loc) => loc.id_hris_location); + } + } + return { + remote_id: company.uuid || null, + legal_name: company.name || null, + display_name: company.trade_name || null, + eins: company.ein ? [company.ein] : [], + remote_data: company, + ...opts, + }; + } +} diff --git a/packages/api/src/hris/company/services/gusto/types.ts b/packages/api/src/hris/company/services/gusto/types.ts new file mode 100644 index 000000000..3bfa8539d --- /dev/null +++ b/packages/api/src/hris/company/services/gusto/types.ts @@ -0,0 +1,76 @@ +export type GustoCompanyOutput = Partial<{ + ein: string; // The Federal Employer Identification Number of the company. + entity_type: + | 'C-Corporation' + | 'S-Corporation' + | 'Sole proprietor' + | 'LLC' + | 'LLP' + | 'Limited partnership' + | 'Co-ownership' + | 'Association' + | 'Trusteeship' + | 'General partnership' + | 'Joint venture' + | 'Non-Profit'; // The tax payer type of the company. + tier: + | 'simple' + | 'plus' + | 'premium' + | 'core' + | 'complete' + | 'concierge' + | 'contractor_only' + | 'basic' + | null; // The Gusto product tier of the company. + is_suspended: boolean; // Whether or not the company is suspended in Gusto. + company_status: 'Approved' | 'Not Approved' | 'Suspended'; // The status of the company in Gusto. + uuid: string; // A unique identifier of the company in Gusto. + name: string; // The name of the company. + slug: string; // The slug of the name of the company. + trade_name: string; // The trade name of the company. + is_partner_managed: boolean; // Whether the company is fully managed by a partner via the API + pay_schedule_type: + | 'single' + | 'hourly_salaried' + | 'by_employee' + | 'by_department'; // The pay schedule assignment type. + join_date: string; // Company's first invoiceable event date + funding_type: 'ach' | 'reverse_wire' | 'wire_in' | 'brex'; // Company's default funding type + locations: Array
; // The locations of the company, with status + compensations: { + hourly: CompensationRate[]; // The available hourly compensation rates for the company. + fixed: CompensationRate[]; // The available fixed compensation rates for the company. + }; + paid_time_off: PaidTimeOff[]; // The available types of paid time off for the company. + primary_signatory: Person; // The primary signatory of the company. + primary_payroll_admin: Omit; // The primary payroll admin of the company. +}>; + +type Address = { + street_1: string; + street_2: string | null; + city: string; + state: string; + zip: string; + country: string; // Defaults to USA +}; + +type CompensationRate = { + name: string; + multiple?: number; // For hourly compensation + fixed?: number; // For fixed compensation +}; + +type PaidTimeOff = { + name: string; +}; + +type Person = { + first_name: string; + middle_initial?: string; + last_name: string; + phone: string; + email: string; + home_address?: Address; +}; diff --git a/packages/api/src/hris/company/sync/sync.processor.ts b/packages/api/src/hris/company/sync/sync.processor.ts new file mode 100644 index 000000000..e228206f7 --- /dev/null +++ b/packages/api/src/hris/company/sync/sync.processor.ts @@ -0,0 +1,19 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; +import { Queues } from '@@core/@core-services/queues/types'; + +@Processor(Queues.SYNC_JOBS_WORKER) +export class SyncProcessor { + constructor(private syncService: SyncService) {} + + @Process('hris-sync-companies') + async handleSyncCompanies(job: Job) { + try { + console.log(`Processing queue -> hris-sync-companies ${job.id}`); + await this.syncService.kickstartSync(); + } catch (error) { + console.error('Error syncing hris companies', error); + } + } +} diff --git a/packages/api/src/hris/company/sync/sync.service.ts b/packages/api/src/hris/company/sync/sync.service.ts index 8c859fad2..e4233d9a8 100644 --- a/packages/api/src/hris/company/sync/sync.service.ts +++ b/packages/api/src/hris/company/sync/sync.service.ts @@ -10,6 +10,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedHrisCompanyOutput } from '../types/model.unified'; import { ICompanyService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { HRIS_PROVIDERS } from '@panora/shared'; +import { hris_companies as HrisCompany } from '@prisma/client'; +import { OriginalCompanyOutput } from '@@core/utils/types/original/original.hris'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -19,23 +25,150 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('hris', 'company', this); } - saveToDb( + + async onModuleInit() { + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing companies...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of HRIS_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: ICompanyService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedHrisCompanyOutput, + OriginalCompanyOutput, + ICompanyService + >(integrationId, linkedUserId, 'hris', 'company', service, []); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + companies: UnifiedHrisCompanyOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const companyResults: HrisCompany[] = []; - async onModuleInit() { - // Initialization logic - } + for (let i = 0; i < companies.length; i++) { + const company = companies[i]; + const originId = company.remote_id; + + let existingCompany = await this.prisma.hris_companies.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const companyData = { + legal_name: company.legal_name, + display_name: company.display_name, + eins: company.eins || [], + remote_id: originId, + remote_created_at: company.remote_created_at + ? new Date(company.remote_created_at) + : null, + modified_at: new Date(), + remote_was_deleted: company.remote_was_deleted || false, + }; + + if (existingCompany) { + existingCompany = await this.prisma.hris_companies.update({ + where: { id_hris_company: existingCompany.id_hris_company }, + data: companyData, + }); + } else { + existingCompany = await this.prisma.hris_companies.create({ + data: { + ...companyData, + id_hris_company: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } - // Additional methods and logic + if (company.locations) { + for (const loc of company.locations) { + await this.prisma.hris_locations.update({ + where: { + id_hris_location: loc, + }, + data: { + id_hris_company: existingCompany.id_hris_company, + }, + }); + } + } + + companyResults.push(existingCompany); + + // Process field mappings + await this.ingestService.processFieldMappings( + company.field_mappings, + existingCompany.id_hris_company, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingCompany.id_hris_company, + remote_data[i], + ); + } + + return companyResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/hris/company/types/index.ts b/packages/api/src/hris/company/types/index.ts index 1533f6255..d07fe0dc7 100644 --- a/packages/api/src/hris/company/types/index.ts +++ b/packages/api/src/hris/company/types/index.ts @@ -5,17 +5,10 @@ import { } from './model.unified'; import { OriginalCompanyOutput } from '@@core/utils/types/original/original.hris'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface ICompanyService { - addCompany( - companyData: DesunifyReturnType, - linkedUserId: string, - ): Promise>; - - syncCompanys( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface ICompanyMapper { diff --git a/packages/api/src/hris/company/types/model.unified.ts b/packages/api/src/hris/company/types/model.unified.ts index bccfa4ea6..038a0a441 100644 --- a/packages/api/src/hris/company/types/model.unified.ts +++ b/packages/api/src/hris/company/types/model.unified.ts @@ -1,3 +1,142 @@ -export class UnifiedHrisCompanyInput {} +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsArray, + IsDateString, + IsBoolean, +} from 'class-validator'; -export class UnifiedHrisCompanyOutput extends UnifiedHrisCompanyInput {} +export class UnifiedHrisCompanyInput { + @ApiPropertyOptional({ + type: String, + example: 'Acme Corporation', + nullable: true, + description: 'The legal name of the company', + }) + @IsString() + @IsOptional() + legal_name?: string; + + @ApiPropertyOptional({ + type: [String], + example: ['801f9ede-c698-4e66-a7fc-48d19eebaa4f'], + nullable: true, + description: 'UUIDs of the of the Location associated with the company', + }) + @IsString() + @IsOptional() + locations?: string[]; + + @ApiPropertyOptional({ + type: String, + example: 'Acme Corp', + nullable: true, + description: 'The display name of the company', + }) + @IsString() + @IsOptional() + display_name?: string; + + @ApiPropertyOptional({ + type: [String], + example: ['12-3456789', '98-7654321'], + nullable: true, + description: 'The Employer Identification Numbers (EINs) of the company', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + eins?: string[]; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedHrisCompanyOutput extends UnifiedHrisCompanyInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the company record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'company_1234', + nullable: true, + description: 'The remote ID of the company in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the company in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: + 'The date when the company was created in the 3rd party system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The created date of the company record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The last modified date of the company record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; + + @ApiPropertyOptional({ + type: Boolean, + example: false, + nullable: true, + description: 'Indicates if the company was deleted in the remote system', + }) + @IsBoolean() + @IsOptional() + remote_was_deleted?: boolean; +} diff --git a/packages/api/src/hris/dependent/dependent.controller.ts b/packages/api/src/hris/dependent/dependent.controller.ts index 391da2176..90fa2e949 100644 --- a/packages/api/src/hris/dependent/dependent.controller.ts +++ b/packages/api/src/hris/dependent/dependent.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -33,7 +35,6 @@ import { ApiPaginatedResponse, } from '@@core/utils/dtos/openapi.respone.dto'; - @ApiTags('hris/dependents') @Controller('hris/dependents') export class DependentController { @@ -57,6 +58,7 @@ export class DependentController { }) @ApiPaginatedResponse(UnifiedHrisDependentOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getDependents( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/hris/dependent/dependent.module.ts b/packages/api/src/hris/dependent/dependent.module.ts index 164fe3ad6..22423cbdc 100644 --- a/packages/api/src/hris/dependent/dependent.module.ts +++ b/packages/api/src/hris/dependent/dependent.module.ts @@ -1,33 +1,21 @@ import { Module } from '@nestjs/common'; import { DependentController } from './dependent.controller'; -import { SyncService } from './sync/sync.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { DependentService } from './services/dependent.service'; import { ServiceRegistry } from './services/registry.service'; -import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; - -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { BullModule } from '@nestjs/bull'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { SyncService } from './sync/sync.service'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; - +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; - +import { Utils } from '@hris/@lib/@utils'; @Module({ controllers: [DependentController], providers: [ DependentService, - + Utils, CoreUnification, - SyncService, WebhookService, - ServiceRegistry, - IngestDataService, /* PROVIDERS SERVICES */ ], diff --git a/packages/api/src/hris/dependent/services/dependent.service.ts b/packages/api/src/hris/dependent/services/dependent.service.ts index 035b8fc4c..378b22dc7 100644 --- a/packages/api/src/hris/dependent/services/dependent.service.ts +++ b/packages/api/src/hris/dependent/services/dependent.service.ts @@ -1,20 +1,11 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { - UnifiedHrisDependentInput, - UnifiedHrisDependentOutput, -} from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; +import { UnifiedHrisDependentOutput } from '../types/model.unified'; import { ServiceRegistry } from './registry.service'; -import { OriginalDependentOutput } from '@@core/utils/types/original/original.hris'; - -import { IDependentService } from '../types'; @Injectable() export class DependentService { @@ -29,14 +20,83 @@ export class DependentService { } async getDependent( - id_dependenting_dependent: string, + id_hris_dependent: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const dependent = await this.prisma.hris_dependents.findUnique({ + where: { id_hris_dependents: id_hris_dependent }, + }); + + if (!dependent) { + throw new Error(`Dependent with ID ${id_hris_dependent} not found.`); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: dependent.id_hris_dependents }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedDependent: UnifiedHrisDependentOutput = { + id: dependent.id_hris_dependents, + first_name: dependent.first_name, + last_name: dependent.last_name, + middle_name: dependent.middle_name, + relationship: dependent.relationship, + date_of_birth: dependent.date_of_birth, + gender: dependent.gender, + phone_number: dependent.phone_number, + home_location: dependent.home_location, + is_student: dependent.is_student, + ssn: dependent.ssn, + employee_id: dependent.id_hris_employee, + field_mappings: field_mappings, + remote_id: dependent.remote_id, + remote_created_at: dependent.remote_created_at, + created_at: dependent.created_at, + modified_at: dependent.modified_at, + remote_was_deleted: dependent.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: dependent.id_hris_dependents }, + }); + unifiedDependent.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.dependent.pull', + method: 'GET', + url: '/hris/dependent', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedDependent; + } catch (error) { + throw error; + } } async getDependents( @@ -47,7 +107,94 @@ export class DependentService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedHrisDependentOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const dependents = await this.prisma.hris_dependents.findMany({ + take: limit + 1, + cursor: cursor ? { id_hris_dependents: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = dependents.length > limit; + if (hasNextPage) dependents.pop(); + + const unifiedDependents = await Promise.all( + dependents.map(async (dependent) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: dependent.id_hris_dependents }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedDependent: UnifiedHrisDependentOutput = { + id: dependent.id_hris_dependents, + first_name: dependent.first_name, + last_name: dependent.last_name, + middle_name: dependent.middle_name, + relationship: dependent.relationship, + date_of_birth: dependent.date_of_birth, + gender: dependent.gender, + phone_number: dependent.phone_number, + home_location: dependent.home_location, + is_student: dependent.is_student, + ssn: dependent.ssn, + employee_id: dependent.id_hris_employee, + field_mappings: field_mappings, + remote_id: dependent.remote_id, + remote_created_at: dependent.remote_created_at, + created_at: dependent.created_at, + modified_at: dependent.modified_at, + remote_was_deleted: dependent.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: dependent.id_hris_dependents }, + }); + unifiedDependent.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedDependent; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.dependent.pull', + method: 'GET', + url: '/hris/dependents', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedDependents, + next_cursor: hasNextPage + ? dependents[dependents.length - 1].id_hris_dependents + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/hris/dependent/sync/sync.processor.ts b/packages/api/src/hris/dependent/sync/sync.processor.ts new file mode 100644 index 000000000..0eed99e3c --- /dev/null +++ b/packages/api/src/hris/dependent/sync/sync.processor.ts @@ -0,0 +1,19 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; +import { Queues } from '@@core/@core-services/queues/types'; + +@Processor(Queues.SYNC_JOBS_WORKER) +export class SyncProcessor { + constructor(private syncService: SyncService) {} + + @Process('hris-sync-dependents') + async handleSyncCompanies(job: Job) { + try { + console.log(`Processing queue -> hris-sync-dependents ${job.id}`); + await this.syncService.kickstartSync(); + } catch (error) { + console.error('Error syncing hris dependents', error); + } + } +} diff --git a/packages/api/src/hris/dependent/sync/sync.service.ts b/packages/api/src/hris/dependent/sync/sync.service.ts index ba70774a1..8bacd8f15 100644 --- a/packages/api/src/hris/dependent/sync/sync.service.ts +++ b/packages/api/src/hris/dependent/sync/sync.service.ts @@ -10,6 +10,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedHrisDependentOutput } from '../types/model.unified'; import { IDependentService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { HRIS_PROVIDERS } from '@panora/shared'; +import { hris_dependents as HrisDependent } from '@prisma/client'; +import { OriginalDependentOutput } from '@@core/utils/types/original/original.hris'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -19,23 +25,147 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('hris', 'dependent', this); } - saveToDb( + + async onModuleInit() { + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing dependents...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of HRIS_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IDependentService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedHrisDependentOutput, + OriginalDependentOutput, + IDependentService + >(integrationId, linkedUserId, 'hris', 'dependent', service, []); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + dependents: UnifiedHrisDependentOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const dependentResults: HrisDependent[] = []; - async onModuleInit() { - // Initialization logic - } + for (let i = 0; i < dependents.length; i++) { + const dependent = dependents[i]; + const originId = dependent.remote_id; + + let existingDependent = await this.prisma.hris_dependents.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const dependentData = { + first_name: dependent.first_name, + last_name: dependent.last_name, + middle_name: dependent.middle_name, + relationship: dependent.relationship, + date_of_birth: dependent.date_of_birth + ? new Date(dependent.date_of_birth) + : null, + gender: dependent.gender, + phone_number: dependent.phone_number, + home_location: dependent.home_location, + is_student: dependent.is_student, + ssn: dependent.ssn, + id_hris_employee: dependent.employee_id, + remote_id: originId, + remote_created_at: dependent.remote_created_at + ? new Date(dependent.remote_created_at) + : null, + modified_at: new Date(), + remote_was_deleted: dependent.remote_was_deleted || false, + }; - // Additional methods and logic + if (existingDependent) { + existingDependent = await this.prisma.hris_dependents.update({ + where: { id_hris_dependents: existingDependent.id_hris_dependents }, + data: dependentData, + }); + } else { + existingDependent = await this.prisma.hris_dependents.create({ + data: { + ...dependentData, + id_hris_dependents: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + dependentResults.push(existingDependent); + + // Process field mappings + await this.ingestService.processFieldMappings( + dependent.field_mappings, + existingDependent.id_hris_dependents, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingDependent.id_hris_dependents, + remote_data[i], + ); + } + + return dependentResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/hris/dependent/types/index.ts b/packages/api/src/hris/dependent/types/index.ts index b31865e8b..deea11064 100644 --- a/packages/api/src/hris/dependent/types/index.ts +++ b/packages/api/src/hris/dependent/types/index.ts @@ -1,18 +1,14 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { UnifiedHrisDependentInput, UnifiedHrisDependentOutput } from './model.unified'; +import { + UnifiedHrisDependentInput, + UnifiedHrisDependentOutput, +} from './model.unified'; import { OriginalDependentOutput } from '@@core/utils/types/original/original.hris'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IDependentService { - addDependent( - dependentData: DesunifyReturnType, - linkedUserId: string, - ): Promise>; - - syncDependents( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IDependentMapper { diff --git a/packages/api/src/hris/dependent/types/model.unified.ts b/packages/api/src/hris/dependent/types/model.unified.ts index d43929548..70eedd34e 100644 --- a/packages/api/src/hris/dependent/types/model.unified.ts +++ b/packages/api/src/hris/dependent/types/model.unified.ts @@ -1,3 +1,222 @@ -export class UnifiedHrisDependentInput {} +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsDateString, + IsBoolean, +} from 'class-validator'; -export class UnifiedHrisDependentOutput extends UnifiedHrisDependentInput {} +export type Gender = + | 'MALE' + | 'FEMALE' + | 'NON-BINARY' + | 'OTHER' + | 'PREFER_NOT_TO_DISCLOSE'; + +export type Relationship = 'CHILD' | 'SPOUSE' | 'DOMESTIC_PARTNER'; + +export class UnifiedHrisDependentInput { + @ApiPropertyOptional({ + type: String, + example: 'John', + nullable: true, + description: 'The first name of the dependent', + }) + @IsString() + @IsOptional() + first_name?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Doe', + nullable: true, + description: 'The last name of the dependent', + }) + @IsString() + @IsOptional() + last_name?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Michael', + nullable: true, + description: 'The middle name of the dependent', + }) + @IsString() + @IsOptional() + middle_name?: string; + + @ApiPropertyOptional({ + type: String, + example: 'CHILD', + enum: ['CHILD', 'SPOUSE', 'DOMESTIC_PARTNER'], + nullable: true, + description: 'The relationship of the dependent to the employee', + }) + @IsString() + @IsOptional() + relationship?: Relationship | string; + + @ApiPropertyOptional({ + type: Date, + example: '2020-01-01', + nullable: true, + description: 'The date of birth of the dependent', + }) + @IsDateString() + @IsOptional() + date_of_birth?: Date; + + @ApiPropertyOptional({ + type: String, + example: 'MALE', + enum: ['MALE', 'FEMALE', 'NON-BINARY', 'OTHER', 'PREFER_NOT_TO_DISCLOSE'], + nullable: true, + description: 'The gender of the dependent', + }) + @IsString() + @IsOptional() + gender?: Gender | string; + + @ApiPropertyOptional({ + type: String, + example: '+1234567890', + nullable: true, + description: 'The phone number of the dependent', + }) + @IsString() + @IsOptional() + phone_number?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the home location', + }) + @IsUUID() + @IsOptional() + home_location?: string; + + @ApiPropertyOptional({ + type: Boolean, + example: true, + nullable: true, + description: 'Indicates if the dependent is a student', + }) + @IsBoolean() + @IsOptional() + is_student?: boolean; + + @ApiPropertyOptional({ + type: String, + example: '123-45-6789', + nullable: true, + description: 'The Social Security Number of the dependent', + }) + @IsString() + @IsOptional() + ssn?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated employee', + }) + @IsUUID() + @IsOptional() + employee_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedHrisDependentOutput extends UnifiedHrisDependentInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the dependent record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'dependent_1234', + nullable: true, + description: + 'The remote ID of the dependent in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the dependent in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: + 'The date when the dependent was created in the 3rd party system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The created date of the dependent record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The last modified date of the dependent record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; + + @ApiPropertyOptional({ + type: Boolean, + example: false, + nullable: true, + description: 'Indicates if the dependent was deleted in the remote system', + }) + @IsBoolean() + @IsOptional() + remote_was_deleted?: boolean; +} diff --git a/packages/api/src/hris/employee/employee.controller.ts b/packages/api/src/hris/employee/employee.controller.ts index 63d3ac95e..d0274e026 100644 --- a/packages/api/src/hris/employee/employee.controller.ts +++ b/packages/api/src/hris/employee/employee.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -34,7 +36,6 @@ import { ApiPostCustomResponse, } from '@@core/utils/dtos/openapi.respone.dto'; - @ApiTags('hris/employees') @Controller('hris/employees') export class EmployeeController { @@ -58,6 +59,7 @@ export class EmployeeController { }) @ApiPaginatedResponse(UnifiedHrisEmployeeOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getEmployees( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/hris/employee/employee.module.ts b/packages/api/src/hris/employee/employee.module.ts index 38026ec03..ed03dd83f 100644 --- a/packages/api/src/hris/employee/employee.module.ts +++ b/packages/api/src/hris/employee/employee.module.ts @@ -1,35 +1,27 @@ import { Module } from '@nestjs/common'; import { EmployeeController } from './employee.controller'; -import { SyncService } from './sync/sync.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { EmployeeService } from './services/employee.service'; import { ServiceRegistry } from './services/registry.service'; -import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; - -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { BullModule } from '@nestjs/bull'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { SyncService } from './sync/sync.service'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; - +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; - +import { GustoEmployeeMapper } from './services/gusto/mappers'; +import { GustoService } from './services/gusto'; +import { Utils } from '@hris/@lib/@utils'; @Module({ controllers: [EmployeeController], providers: [ EmployeeService, CoreUnification, - SyncService, - + Utils, WebhookService, - ServiceRegistry, - IngestDataService, + GustoEmployeeMapper, /* PROVIDERS SERVICES */ + GustoService, ], exports: [SyncService], }) diff --git a/packages/api/src/hris/employee/services/employee.service.ts b/packages/api/src/hris/employee/services/employee.service.ts index 91339c353..7fe253309 100644 --- a/packages/api/src/hris/employee/services/employee.service.ts +++ b/packages/api/src/hris/employee/services/employee.service.ts @@ -1,20 +1,20 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; import { UnifiedHrisEmployeeInput, UnifiedHrisEmployeeOutput, } from '../types/model.unified'; - -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; +import { ApiResponse } from '@@core/utils/types'; import { OriginalEmployeeOutput } from '@@core/utils/types/original/original.hris'; - +import { HrisObject } from '@panora/shared'; import { IEmployeeService } from '../types'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class EmployeeService { @@ -23,11 +23,21 @@ export class EmployeeService { private logger: LoggerService, private webhook: WebhookService, private fieldMappingService: FieldMappingService, + private coreUnification: CoreUnification, + private ingestService: IngestDataService, private serviceRegistry: ServiceRegistry, ) { this.logger.setContext(EmployeeService.name); } + async validateLinkedUser(linkedUserId: string) { + const linkedUser = await this.prisma.linked_users.findUnique({ + where: { id_linked_user: linkedUserId }, + }); + if (!linkedUser) throw new ReferenceError('Linked User Not Found'); + return linkedUser; + } + async addEmployee( unifiedEmployeeData: UnifiedHrisEmployeeInput, connection_id: string, @@ -36,18 +46,240 @@ export class EmployeeService { linkedUserId: string, remote_data?: boolean, ): Promise { - return; + try { + const linkedUser = await this.validateLinkedUser(linkedUserId); + + const desunifiedObject = + await this.coreUnification.desunify({ + sourceObject: unifiedEmployeeData, + targetType: HrisObject.employee, + providerName: integrationId, + vertical: 'hris', + customFieldMappings: [], + }); + + const service: IEmployeeService = + this.serviceRegistry.getService(integrationId); + const resp: ApiResponse = + await service.addEmployee(desunifiedObject, linkedUserId); + + const unifiedObject = (await this.coreUnification.unify< + OriginalEmployeeOutput[] + >({ + sourceObject: [resp.data], + targetType: HrisObject.employee, + providerName: integrationId, + vertical: 'hris', + connectionId: connection_id, + customFieldMappings: [], + })) as UnifiedHrisEmployeeOutput[]; + + const source_employee = resp.data; + const target_employee = unifiedObject[0]; + + const unique_hris_employee_id = await this.saveOrUpdateEmployee( + target_employee, + connection_id, + ); + + await this.ingestService.processRemoteData( + unique_hris_employee_id, + source_employee, + ); + + const result_employee = await this.getEmployee( + unique_hris_employee_id, + undefined, + undefined, + connection_id, + project_id, + remote_data, + ); + + const status_resp = resp.statusCode === 201 ? 'success' : 'fail'; + const event = await this.prisma.events.create({ + data: { + id_connection: connection_id, + id_project: project_id, + id_event: uuidv4(), + status: status_resp, + type: 'hris.employee.push', + method: 'POST', + url: '/hris/employees', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + + await this.webhook.dispatchWebhook( + result_employee, + 'hris.employee.created', + linkedUser.id_project, + event.id_event, + ); + + return result_employee; + } catch (error) { + throw error; + } + } + + async saveOrUpdateEmployee( + employee: UnifiedHrisEmployeeOutput, + connectionId: string, + ): Promise { + const existingEmployee = await this.prisma.hris_employees.findFirst({ + where: { remote_id: employee.remote_id, id_connection: connectionId }, + }); + + const data: any = { + groups: employee.groups || [], + employee_number: employee.employee_number, + id_hris_company: employee.company_id, + first_name: employee.first_name, + last_name: employee.last_name, + preferred_name: employee.preferred_name, + display_full_name: employee.display_full_name, + username: employee.username, + work_email: employee.work_email, + personal_email: employee.personal_email, + mobile_phone_number: employee.mobile_phone_number, + employments: employee.employments || [], + ssn: employee.ssn, + gender: employee.gender, + ethnicity: employee.ethnicity, + marital_status: employee.marital_status, + date_of_birth: employee.date_of_birth, + start_date: employee.start_date, + employment_status: employee.employment_status, + termination_date: employee.termination_date, + avatar_url: employee.avatar_url, + modified_at: new Date(), + }; + + if (existingEmployee) { + const res = await this.prisma.hris_employees.update({ + where: { id_hris_employee: existingEmployee.id_hris_employee }, + data: data, + }); + + return res.id_hris_employee; + } else { + data.created_at = new Date(); + data.remote_id = employee.remote_id; + data.id_connection = connectionId; + data.id_hris_employee = uuidv4(); + data.remote_was_deleted = employee.remote_was_deleted ?? false; + data.remote_created_at = employee.remote_created_at + ? new Date(employee.remote_created_at) + : null; + + const newEmployee = await this.prisma.hris_employees.create({ + data: data, + }); + + return newEmployee.id_hris_employee; + } } async getEmployee( - id_employeeing_employee: string, + id_hris_employee: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const employee = await this.prisma.hris_employees.findUnique({ + where: { id_hris_employee: id_hris_employee }, + }); + + if (!employee) { + throw new Error(`Employee with ID ${id_hris_employee} not found.`); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: employee.id_hris_employee }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const locations = await this.prisma.hris_locations.findMany({ + where: { + id_hris_employee: employee.id_hris_employee, + }, + }); + + const unifiedEmployee: UnifiedHrisEmployeeOutput = { + id: employee.id_hris_employee, + groups: employee.groups, + employee_number: employee.employee_number, + company_id: employee.id_hris_company, + first_name: employee.first_name, + last_name: employee.last_name, + preferred_name: employee.preferred_name, + display_full_name: employee.display_full_name, + username: employee.username, + work_email: employee.work_email, + personal_email: employee.personal_email, + mobile_phone_number: employee.mobile_phone_number, + employments: employee.employments, + ssn: employee.ssn, + manager_id: employee.manager, + gender: employee.gender, + ethnicity: employee.ethnicity, + marital_status: employee.marital_status, + date_of_birth: employee.date_of_birth, + start_date: employee.start_date, + employment_status: employee.employment_status, + termination_date: employee.termination_date, + avatar_url: employee.avatar_url, + locations: locations.map((loc) => loc.id_hris_location), + field_mappings: field_mappings, + remote_id: employee.remote_id, + remote_created_at: employee.remote_created_at, + created_at: employee.created_at, + modified_at: employee.modified_at, + remote_was_deleted: employee.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: employee.id_hris_employee }, + }); + unifiedEmployee.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.employee.pull', + method: 'GET', + url: '/hris/employee', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedEmployee; + } catch (error) { + throw error; + } } async getEmployees( @@ -58,7 +290,108 @@ export class EmployeeService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedHrisEmployeeOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const employees = await this.prisma.hris_employees.findMany({ + take: limit + 1, + cursor: cursor ? { id_hris_employee: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = employees.length > limit; + if (hasNextPage) employees.pop(); + + const unifiedEmployees = await Promise.all( + employees.map(async (employee) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: employee.id_hris_employee }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const locations = await this.prisma.hris_locations.findMany({ + where: { + id_hris_employee: employee.id_hris_employee, + }, + }); + + const unifiedEmployee: UnifiedHrisEmployeeOutput = { + id: employee.id_hris_employee, + groups: employee.groups, + employee_number: employee.employee_number, + company_id: employee.id_hris_company, + first_name: employee.first_name, + last_name: employee.last_name, + preferred_name: employee.preferred_name, + display_full_name: employee.display_full_name, + username: employee.username, + locations: locations.map((loc) => loc.id_hris_location), + manager_id: employee.manager, + work_email: employee.work_email, + personal_email: employee.personal_email, + mobile_phone_number: employee.mobile_phone_number, + employments: employee.employments, + ssn: employee.ssn, + gender: employee.gender, + ethnicity: employee.ethnicity, + marital_status: employee.marital_status, + date_of_birth: employee.date_of_birth, + start_date: employee.start_date, + employment_status: employee.employment_status, + termination_date: employee.termination_date, + avatar_url: employee.avatar_url, + field_mappings: field_mappings, + remote_id: employee.remote_id, + remote_created_at: employee.remote_created_at, + }; + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: employee.id_hris_employee }, + }); + unifiedEmployee.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedEmployee; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.employee.pull', + method: 'GET', + url: '/hris/employees', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedEmployees, + next_cursor: hasNextPage + ? employees[employees.length - 1].id_hris_employee + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/hris/employee/services/gusto/index.ts b/packages/api/src/hris/employee/services/gusto/index.ts new file mode 100644 index 000000000..942f1a548 --- /dev/null +++ b/packages/api/src/hris/employee/services/gusto/index.ts @@ -0,0 +1,72 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { HrisObject } from '@hris/@lib/@types'; +import { IEmployeeService } from '@hris/employee/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { GustoEmployeeOutput } from './types'; + +@Injectable() +export class GustoService implements IEmployeeService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + HrisObject.employee.toUpperCase() + ':' + GustoService.name, + ); + this.registry.registerService('gusto', this); + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId, id_company } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'gusto', + vertical: 'hris', + }, + }); + + const company = await this.prisma.hris_companies.findUnique({ + where: { + id_hris_company: id_company as string, + }, + select: { + remote_id: true, + }, + }); + + const resp = await axios.get( + `${connection.account_url}/v1/companies/${company.remote_id}/employees`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced gusto employees !`); + + return { + data: resp.data, + message: 'Gusto employees retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/hris/employee/services/gusto/mappers.ts b/packages/api/src/hris/employee/services/gusto/mappers.ts new file mode 100644 index 000000000..6ea34d7f6 --- /dev/null +++ b/packages/api/src/hris/employee/services/gusto/mappers.ts @@ -0,0 +1,139 @@ +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { Injectable } from '@nestjs/common'; +import { GustoEmployeeOutput } from './types'; +import { + UnifiedHrisEmployeeInput, + UnifiedHrisEmployeeOutput, +} from '@hris/employee/types/model.unified'; +import { IEmployeeMapper } from '@hris/employee/types'; +import { Utils } from '@hris/@lib/@utils'; +import { Job } from 'bull'; +import { HrisObject, TicketingObject } from '@panora/shared'; +import { ZendeskTagOutput } from '@ticketing/tag/services/zendesk/types'; +import axios from 'axios'; +import { UnifiedHrisLocationOutput } from '@hris/location/types/model.unified'; +import { UnifiedHrisEmploymentOutput } from '@hris/employment/types/model.unified'; +import { GustoEmploymentOutput } from '@hris/employment/services/gusto/types'; + +@Injectable() +export class GustoEmployeeMapper implements IEmployeeMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService('hris', 'employee', 'gusto', this); + } + + async desunify( + source: UnifiedHrisEmployeeInput, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return; + } + + async unify( + source: GustoEmployeeOutput | GustoEmployeeOutput[], + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleEmployeeToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((employee) => + this.mapSingleEmployeeToUnified( + employee, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleEmployeeToUnified( + employee: GustoEmployeeOutput, + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + const opts: any = {}; + if (employee.company_uuid) { + const company_id = await this.utils.getCompanyUuidFromRemoteId( + employee.company_uuid, + connectionId, + ); + if (company_id) { + opts.company_id = company_id; + } + } + if (employee.manager_uuid) { + const manager_id = await this.utils.getEmployeeUuidFromRemoteId( + employee.manager_uuid, + connectionId, + ); + if (manager_id) { + opts.manager_id = manager_id; + } + } + + if (employee.jobs) { + const compensationObjects = employee.jobs.map((job) => { + const compensation = + job.compensations.find( + (compensation) => + compensation.uuid === job.current_compensation_uuid, + ) || null; + + return { + ...compensation, + title: job.title, + }; + }); + const employments = await this.ingestService.ingestData< + UnifiedHrisEmploymentOutput, + GustoEmploymentOutput + >( + compensationObjects, + 'gusto', + connectionId, + 'hris', + HrisObject.employment, + [], + ); + if (employments) { + opts.employments = employments.map((emp) => emp.id_hris_employment); + } + } + + const primaryJob = employee.jobs.find((job) => job.primary); + + return { + remote_id: employee.uuid, + remote_data: employee, + first_name: employee.first_name, + last_name: employee.last_name, + preferred_name: employee.preferred_first_name, + display_full_name: `${employee.first_name} ${employee.last_name}`, + work_email: employee.work_email, + personal_email: employee.email, + mobile_phone_number: employee.phone, + start_date: primaryJob ? new Date(primaryJob.hire_date) : null, + termination_date: + employee.terminations.length > 0 + ? new Date(employee.terminations[0].effective_date) + : null, + employment_status: employee.current_employment_status, + date_of_birth: employee.date_of_birth + ? new Date(employee.date_of_birth) + : null, + ...opts, + }; + } +} diff --git a/packages/api/src/hris/employee/services/gusto/types.ts b/packages/api/src/hris/employee/services/gusto/types.ts new file mode 100644 index 000000000..881500b5f --- /dev/null +++ b/packages/api/src/hris/employee/services/gusto/types.ts @@ -0,0 +1,123 @@ +export type GustoEmployeeOutput = { + uuid: string; // The UUID of the employee in Gusto. + first_name: string; // The first name of the employee. + middle_initial: string | null; // The middle initial of the employee. + last_name: string; // The last name of the employee. + email: string | null; // The personal email address of the employee. + company_uuid: string; // The UUID of the company the employee is employed by. + manager_uuid: string; // The UUID of the employee's manager. + version: string; // The current version of the employee. + department: string | null; // The employee's department in the company. + terminated: boolean; // Whether the employee is terminated. + two_percent_shareholder: boolean; // Whether the employee is a two percent shareholder of the company. + onboarded: boolean; // Whether the employee has completed onboarding. + onboarding_status: + | 'onboarding_completed' + | 'admin_onboarding_incomplete' + | 'self_onboarding_pending_invite' + | 'self_onboarding_invited' + | 'self_onboarding_invited_started' + | 'self_onboarding_invited_overdue' + | 'self_onboarding_completed_by_employee' + | 'self_onboarding_awaiting_admin_review'; // The current onboarding status of the employee. + jobs: Job[]; // The jobs held by the employee. + terminations: Termination[]; // The terminations of the employee. + garnishments: Garnishment[]; // The garnishments of the employee. + custom_fields?: CustomField[]; // Custom fields for the employee. + date_of_birth: string | null; // The date of birth of the employee. + has_ssn: boolean; // Indicates whether the employee has an SSN in Gusto. + ssn: string; // Deprecated. This field always returns an empty string. + phone: string; // The phone number of the employee. + preferred_first_name: string; // The preferred first name of the employee. + payment_method: 'Direct Deposit' | 'Check' | null; // The employee's payment method. + work_email: string | null; // The work email address of the employee. + current_employment_status: + | 'full_time' + | 'part_time_under_twenty_hours' + | 'part_time_twenty_plus_hours' + | 'variable' + | 'seasonal' + | null; // The current employment status of the employee. +}; + +type Job = { + uuid: string; // The UUID of the job. + version: string; // The current version of the job. + employee_uuid: string; // The UUID of the employee to which the job belongs. + hire_date: string; // The date when the employee was hired or rehired for the job. + title: string | null; // The title for the job. + primary: boolean; // Whether this is the employee's primary job. + rate: string; // The current compensation rate of the job. + payment_unit: string; // The payment unit of the current compensation for the job. + current_compensation_uuid: string; // The UUID of the current compensation of the job. + two_percent_shareholder: boolean; // Whether the employee owns at least 2% of the company. + state_wc_covered: boolean; // Whether this job is eligible for workers' compensation coverage in the state of Washington (WA). + state_wc_class_code: string; // The risk class code for workers' compensation in Washington state. + compensations: Compensation[]; // The compensations associated with the job. +}; + +type Compensation = { + uuid: string; // The UUID of the compensation in Gusto. + version: string; // The current version of the compensation. + job_uuid: string; // The UUID of the job to which the compensation belongs. + rate: string; // The dollar amount paid per payment unit. + payment_unit: 'Hour' | 'Week' | 'Month' | 'Year' | 'Paycheck'; // The unit accompanying the compensation rate. + flsa_status: + | 'Exempt' + | 'Salaried Nonexempt' + | 'Nonexempt' + | 'Owner' + | 'Commission Only Exempt' + | 'Commission Only Nonexempt'; // The FLSA status for this compensation. + effective_date: string; // The effective date for this compensation. + adjust_for_minimum_wage: boolean; // Indicates if the compensation could be adjusted to minimum wage during payroll calculation. + eligible_paid_time_off: EligiblePaidTimeOff[]; // The available types of paid time off for the compensation. +}; + +type EligiblePaidTimeOff = { + name: string; // The name of the paid time off type. + policy_name: string; // The name of the time off policy. + policy_uuid: string; // The UUID of the time off policy. + accrual_unit: string; // The unit the PTO type is accrued in. + accrual_rate: string; // The number of accrual units accrued per accrual period. + accrual_method: string; // The accrual method of the time off policy. + accrual_period: string; // The frequency at which the PTO type is accrued. + accrual_balance: string; // The number of accrual units accrued. + maximum_accrual_balance: string | null; // The maximum number of accrual units allowed. + paid_at_termination: boolean; // Whether the accrual balance is paid to the employee upon termination. +}; + +type Termination = { + uuid: string; // The UUID of the termination object. + version: string; // The current version of the termination. + employee_uuid: string; // The UUID of the employee to which this termination is attached. + active: boolean; // Whether the employee's termination has gone into effect. + cancelable: boolean; // Whether the employee's termination is cancelable. + effective_date: string; // The employee's last day of work. + run_termination_payroll: boolean; // Whether the employee should receive their final wages via an off-cycle payroll. +}; + +type Garnishment = { + uuid: string; // The UUID of the garnishment in Gusto. + version: string; // The current version of the garnishment. + employee_uuid: string; // The UUID of the employee to which this garnishment belongs. + active: boolean; // Whether or not this garnishment is currently active. + amount: string; // The amount of the garnishment. + description: string; // The description of the garnishment. + court_ordered: boolean; // Whether the garnishment is court ordered. + times: number | null; // The number of times to apply the garnishment. + recurring: boolean; // Whether the garnishment should recur indefinitely. + annual_maximum: string | null; // The maximum deduction per annum. + pay_period_maximum: string | null; // The maximum deduction per pay period. + deduct_as_percentage: boolean; // Whether the amount should be treated as a percentage to be deducted per pay period. +}; + +type CustomField = { + id: string; // The ID of the custom field. + company_custom_field_id: string; // The ID of the company custom field. + name: string; // The name of the custom field. + type: 'text' | 'currency' | 'number' | 'date' | 'radio'; // Input type for the custom field. + description: string; // The description of the custom field. + value: string; // The value of the custom field. + selection_options: string[] | null; // An array of options for fields of type radio. +}; diff --git a/packages/api/src/hris/employee/sync/sync.processor.ts b/packages/api/src/hris/employee/sync/sync.processor.ts new file mode 100644 index 000000000..2e11413d2 --- /dev/null +++ b/packages/api/src/hris/employee/sync/sync.processor.ts @@ -0,0 +1,19 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; +import { Queues } from '@@core/@core-services/queues/types'; + +@Processor(Queues.SYNC_JOBS_WORKER) +export class SyncProcessor { + constructor(private syncService: SyncService) {} + + @Process('hris-sync-employees') + async handleSyncEmployees(job: Job) { + try { + console.log(`Processing queue -> hris-sync-employees ${job.id}`); + await this.syncService.kickstartSync(); + } catch (error) { + console.error('Error syncing hris employees', error); + } + } +} diff --git a/packages/api/src/hris/employee/sync/sync.service.ts b/packages/api/src/hris/employee/sync/sync.service.ts index ba2698842..0f41f2fdc 100644 --- a/packages/api/src/hris/employee/sync/sync.service.ts +++ b/packages/api/src/hris/employee/sync/sync.service.ts @@ -10,6 +10,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedHrisEmployeeOutput } from '../types/model.unified'; import { IEmployeeService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { HRIS_PROVIDERS } from '@panora/shared'; +import { hris_employees as HrisEmployee } from '@prisma/client'; +import { OriginalEmployeeOutput } from '@@core/utils/types/original/original.hris'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -19,23 +25,169 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('hris', 'employee', this); } - saveToDb( + + async onModuleInit() { + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing employees...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of HRIS_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId, id_company } = param; + const service: IEmployeeService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedHrisEmployeeOutput, + OriginalEmployeeOutput, + IEmployeeService + >(integrationId, linkedUserId, 'hris', 'employee', service, [ + { + param: id_company, + paramName: 'id_company', + shouldPassToService: true, + shouldPassToIngest: true, + }, + ]); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + employees: UnifiedHrisEmployeeOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const employeeResults: HrisEmployee[] = []; - async onModuleInit() { - // Initialization logic - } + for (let i = 0; i < employees.length; i++) { + const employee = employees[i]; + const originId = employee.remote_id; + + let existingEmployee = await this.prisma.hris_employees.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const employeeData = { + groups: employee.groups || [], + employee_number: employee.employee_number, + id_hris_company: employee.company_id, + first_name: employee.first_name, + last_name: employee.last_name, + preferred_name: employee.preferred_name, + display_full_name: employee.display_full_name, + username: employee.username, + work_email: employee.work_email, + personal_email: employee.personal_email, + mobile_phone_number: employee.mobile_phone_number, + employments: employee.employments || [], + ssn: employee.ssn, + gender: employee.gender, + manager_id: employee.manager_id, + ethnicity: employee.ethnicity, + marital_status: employee.marital_status, + date_of_birth: employee.date_of_birth + ? new Date(employee.date_of_birth) + : null, + start_date: employee.start_date + ? new Date(employee.start_date) + : null, + employment_status: employee.employment_status, + termination_date: employee.termination_date + ? new Date(employee.termination_date) + : null, + avatar_url: employee.avatar_url, + remote_id: originId, + remote_created_at: employee.remote_created_at + ? new Date(employee.remote_created_at) + : null, + modified_at: new Date(), + remote_was_deleted: employee.remote_was_deleted || false, + }; - // Additional methods and logic + if (existingEmployee) { + existingEmployee = await this.prisma.hris_employees.update({ + where: { id_hris_employee: existingEmployee.id_hris_employee }, + data: employeeData, + }); + } else { + existingEmployee = await this.prisma.hris_employees.create({ + data: { + ...employeeData, + id_hris_employee: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + employeeResults.push(existingEmployee); + + // Process field mappings + await this.ingestService.processFieldMappings( + employee.field_mappings, + existingEmployee.id_hris_employee, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingEmployee.id_hris_employee, + remote_data[i], + ); + } + + return employeeResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/hris/employee/types/index.ts b/packages/api/src/hris/employee/types/index.ts index 582b158b1..fc44e8583 100644 --- a/packages/api/src/hris/employee/types/index.ts +++ b/packages/api/src/hris/employee/types/index.ts @@ -1,18 +1,19 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { UnifiedHrisEmployeeInput, UnifiedHrisEmployeeOutput } from './model.unified'; +import { + UnifiedHrisEmployeeInput, + UnifiedHrisEmployeeOutput, +} from './model.unified'; import { OriginalEmployeeOutput } from '@@core/utils/types/original/original.hris'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IEmployeeService { - addEmployee( + addEmployee?( employeeData: DesunifyReturnType, linkedUserId: string, ): Promise>; - syncEmployees( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IEmployeeMapper { diff --git a/packages/api/src/hris/employee/types/model.unified.ts b/packages/api/src/hris/employee/types/model.unified.ts index e64967d81..a7c6e8239 100644 --- a/packages/api/src/hris/employee/types/model.unified.ts +++ b/packages/api/src/hris/employee/types/model.unified.ts @@ -1,3 +1,382 @@ -export class UnifiedHrisEmployeeInput {} +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsArray, + IsDateString, + IsEmail, + IsUrl, +} from 'class-validator'; -export class UnifiedHrisEmployeeOutput extends UnifiedHrisEmployeeInput {} +export type Gender = + | 'MALE' + | 'FEMALE' + | 'NON-BINARY' + | 'OTHER' + | 'PREFER_NOT_TO_DISCLOSE'; + +export type Ethnicity = + | 'AMERICAN_INDIAN_OR_ALASKA_NATIVE' + | 'ASIAN_OR_INDIAN_SUBCONTINENT' + | 'BLACK_OR_AFRICAN_AMERICAN' + | 'HISPANIC_OR_LATINO' + | 'NATIVE_HAWAIIAN_OR_OTHER_PACIFIC_ISLANDER' + | 'TWO_OR_MORE_RACES' + | 'WHITE' + | 'PREFER_NOT_TO_DISCLOSE'; + +export type MartialStatus = + | 'SINGLE' + | 'MARRIED_FILING_JOINTLY' + | 'MARRIED_FILING_SEPARATELY' + | 'HEAD_OF_HOUSEHOLD' + | 'QUALIFYING_WIDOW_OR_WIDOWER_WITH_DEPENDENT_CHILD'; + +export type EmploymentStatus = 'ACTIVE' | 'PENDING' | 'INACTIVE'; + +export class UnifiedHrisEmployeeInput { + @ApiPropertyOptional({ + type: [String], + example: ['Group1', 'Group2'], + nullable: true, + description: 'The groups the employee belongs to', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + groups?: string[]; + + @ApiPropertyOptional({ + type: [String], + example: ['801f9ede-c698-4e66-a7fc-48d19eebaa4f'], + nullable: true, + description: 'UUIDs of the of the Location associated with the company', + }) + @IsString() + @IsOptional() + locations?: string[]; + + @ApiPropertyOptional({ + type: String, + example: 'EMP001', + nullable: true, + description: 'The employee number', + }) + @IsString() + @IsOptional() + employee_number?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated company', + }) + @IsUUID() + @IsOptional() + company_id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'John', + nullable: true, + description: 'The first name of the employee', + }) + @IsString() + @IsOptional() + first_name?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Doe', + nullable: true, + description: 'The last name of the employee', + }) + @IsString() + @IsOptional() + last_name?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Johnny', + nullable: true, + description: 'The preferred name of the employee', + }) + @IsString() + @IsOptional() + preferred_name?: string; + + @ApiPropertyOptional({ + type: String, + example: 'John Doe', + nullable: true, + description: 'The full display name of the employee', + }) + @IsString() + @IsOptional() + display_full_name?: string; + + @ApiPropertyOptional({ + type: String, + example: 'johndoe', + nullable: true, + description: 'The username of the employee', + }) + @IsString() + @IsOptional() + username?: string; + + @ApiPropertyOptional({ + type: String, + example: 'john.doe@company.com', + nullable: true, + description: 'The work email of the employee', + }) + @IsEmail() + @IsOptional() + work_email?: string; + + @ApiPropertyOptional({ + type: String, + example: 'john.doe@personal.com', + nullable: true, + description: 'The personal email of the employee', + }) + @IsEmail() + @IsOptional() + personal_email?: string; + + @ApiPropertyOptional({ + type: String, + example: '+1234567890', + nullable: true, + description: 'The mobile phone number of the employee', + }) + @IsString() + @IsOptional() + mobile_phone_number?: string; + + @ApiPropertyOptional({ + type: [String], + example: [ + '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + ], + nullable: true, + description: 'The employments of the employee', + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + employments?: string[]; + + @ApiPropertyOptional({ + type: String, + example: '123-45-6789', + nullable: true, + description: 'The Social Security Number of the employee', + }) + @IsString() + @IsOptional() + ssn?: string; + + @ApiPropertyOptional({ + type: String, + example: 'MALE', + enum: ['MALE', 'FEMALE', 'NON-BINARY', 'OTHER', 'PREFER_NOT_TO_DISCLOSE'], + nullable: true, + description: 'The gender of the employee', + }) + @IsString() + @IsOptional() + gender?: Gender | string; + + @ApiPropertyOptional({ + type: String, + example: 'AMERICAN_INDIAN_OR_ALASKA_NATIVE', + enum: [ + 'AMERICAN_INDIAN_OR_ALASKA_NATIVE', + 'ASIAN_OR_INDIAN_SUBCONTINENT', + 'BLACK_OR_AFRICAN_AMERICAN', + 'HISPANIC_OR_LATINO', + 'NATIVE_HAWAIIAN_OR_OTHER_PACIFIC_ISLANDER', + 'TWO_OR_MORE_RACES', + 'WHITE', + 'PREFER_NOT_TO_DISCLOSE', + ], + nullable: true, + description: 'The ethnicity of the employee', + }) + @IsString() + @IsOptional() + ethnicity?: Ethnicity | string; + + @ApiPropertyOptional({ + type: String, + example: 'Married', + enum: [ + 'SINGLE', + 'MARRIED_FILING_JOINTLY', + 'MARRIED_FILING_SEPARATELY', + 'HEAD_OF_HOUSEHOLD', + 'QUALIFYING_WIDOW_OR_WIDOWER_WITH_DEPENDENT_CHILD', + ], + nullable: true, + description: 'The marital status of the employee', + }) + @IsString() + @IsOptional() + marital_status?: MartialStatus | string; + + @ApiPropertyOptional({ + type: Date, + example: '1990-01-01', + nullable: true, + description: 'The date of birth of the employee', + }) + @IsDateString() + @IsOptional() + date_of_birth?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2020-01-01', + nullable: true, + description: 'The start date of the employee', + }) + @IsDateString() + @IsOptional() + start_date?: Date; + + @ApiPropertyOptional({ + type: String, + example: 'ACTIVE', + enum: ['ACTIVE', 'PENDING', 'INACTIVE'], + nullable: true, + description: 'The employment status of the employee', + }) + @IsString() + @IsOptional() + employment_status?: EmploymentStatus | string; + + @ApiPropertyOptional({ + type: Date, + example: '2025-01-01', + nullable: true, + description: 'The termination date of the employee', + }) + @IsDateString() + @IsOptional() + termination_date?: Date; + + @ApiPropertyOptional({ + type: String, + example: 'https://example.com/avatar.jpg', + nullable: true, + description: "The URL of the employee's avatar", + }) + @IsUrl() + @IsOptional() + avatar_url?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'UUID of the manager (employee) of the employee', + }) + @IsUrl() + @IsOptional() + manager_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedHrisEmployeeOutput extends UnifiedHrisEmployeeInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the employee record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'employee_1234', + nullable: true, + description: + 'The remote ID of the employee in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the employee in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: + 'The date when the employee was created in the 3rd party system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The created date of the employee record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The last modified date of the employee record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; + + @ApiPropertyOptional({ + type: Boolean, + example: false, + nullable: true, + description: 'Indicates if the employee was deleted in the remote system', + }) + @IsOptional() + remote_was_deleted?: boolean; +} diff --git a/packages/api/src/hris/employeepayrollrun/employeepayrollrun.controller.ts b/packages/api/src/hris/employeepayrollrun/employeepayrollrun.controller.ts index 877e7dd27..ec3cddb5e 100644 --- a/packages/api/src/hris/employeepayrollrun/employeepayrollrun.controller.ts +++ b/packages/api/src/hris/employeepayrollrun/employeepayrollrun.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -33,7 +35,6 @@ import { ApiPaginatedResponse, } from '@@core/utils/dtos/openapi.respone.dto'; - @ApiTags('hris/employeepayrollruns') @Controller('hris/employeepayrollruns') export class EmployeePayrollRunController { @@ -57,6 +58,7 @@ export class EmployeePayrollRunController { }) @ApiPaginatedResponse(UnifiedHrisEmployeepayrollrunOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getEmployeePayrollRuns( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/hris/employeepayrollrun/employeepayrollrun.module.ts b/packages/api/src/hris/employeepayrollrun/employeepayrollrun.module.ts index 027bf0108..e918446e9 100644 --- a/packages/api/src/hris/employeepayrollrun/employeepayrollrun.module.ts +++ b/packages/api/src/hris/employeepayrollrun/employeepayrollrun.module.ts @@ -1,32 +1,22 @@ import { Module } from '@nestjs/common'; import { EmployeePayrollRunController } from './employeepayrollrun.controller'; -import { SyncService } from './sync/sync.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { EmployeePayrollRunService } from './services/employeepayrollrun.service'; import { ServiceRegistry } from './services/registry.service'; -import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; - -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { BullModule } from '@nestjs/bull'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { SyncService } from './sync/sync.service'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; - +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { Utils } from '@hris/@lib/@utils'; @Module({ controllers: [EmployeePayrollRunController], providers: [ EmployeePayrollRunService, CoreUnification, - + Utils, SyncService, WebhookService, - ServiceRegistry, - IngestDataService, /* PROVIDERS SERVICES */ ], diff --git a/packages/api/src/hris/employeepayrollrun/services/employeepayrollrun.service.ts b/packages/api/src/hris/employeepayrollrun/services/employeepayrollrun.service.ts index cb9080481..4b0ef884a 100644 --- a/packages/api/src/hris/employeepayrollrun/services/employeepayrollrun.service.ts +++ b/packages/api/src/hris/employeepayrollrun/services/employeepayrollrun.service.ts @@ -1,20 +1,11 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { - UnifiedHrisEmployeepayrollrunInput, - UnifiedHrisEmployeepayrollrunOutput, -} from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; +import { UnifiedHrisEmployeepayrollrunOutput } from '../types/model.unified'; import { ServiceRegistry } from './registry.service'; -import { OriginalEmployeePayrollRunOutput } from '@@core/utils/types/original/original.hris'; - -import { IEmployeePayrollRunService } from '../types'; @Injectable() export class EmployeePayrollRunService { @@ -27,15 +18,110 @@ export class EmployeePayrollRunService { ) { this.logger.setContext(EmployeePayrollRunService.name); } + async getEmployeePayrollRun( - id_employeepayrollruning_employeepayrollrun: string, + id_hris_employee_payroll_run: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const employeePayrollRun = + await this.prisma.hris_employee_payroll_runs.findUnique({ + where: { id_hris_employee_payroll_run: id_hris_employee_payroll_run }, + include: { + hris_employee_payroll_runs_deductions: true, + hris_employee_payroll_runs_earnings: true, + hris_employee_payroll_runs_taxes: true, + }, + }); + + if (!employeePayrollRun) { + throw new Error( + `Employee Payroll Run with ID ${id_hris_employee_payroll_run} not found.`, + ); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: employeePayrollRun.id_hris_employee_payroll_run, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedEmployeePayrollRun: UnifiedHrisEmployeepayrollrunOutput = { + id: employeePayrollRun.id_hris_employee_payroll_run, + employee_id: employeePayrollRun.id_hris_employee, + payroll_run_id: employeePayrollRun.id_hris_payroll_run, + gross_pay: Number(employeePayrollRun.gross_pay), + net_pay: Number(employeePayrollRun.net_pay), + start_date: employeePayrollRun.start_date, + end_date: employeePayrollRun.end_date, + check_date: employeePayrollRun.check_date, + deductions: + employeePayrollRun.hris_employee_payroll_runs_deductions.map((d) => ({ + name: d.name, + employee_deduction: Number(d.employee_deduction), + company_deduction: Number(d.company_deduction), + })), + earnings: employeePayrollRun.hris_employee_payroll_runs_earnings.map( + (e) => ({ + amount: Number(e.amount), + type: e.type, + }), + ), + taxes: employeePayrollRun.hris_employee_payroll_runs_taxes.map((t) => ({ + name: t.name, + amount: Number(t.amount), + employer_tax: t.employer_tax, + })), + field_mappings: field_mappings, + remote_id: employeePayrollRun.remote_id, + remote_created_at: employeePayrollRun.remote_created_at, + created_at: employeePayrollRun.created_at, + modified_at: employeePayrollRun.modified_at, + remote_was_deleted: employeePayrollRun.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: employeePayrollRun.id_hris_employee_payroll_run, + }, + }); + unifiedEmployeePayrollRun.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.employee_payroll_run.pull', + method: 'GET', + url: '/hris/employee_payroll_run', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedEmployeePayrollRun; + } catch (error) { + throw error; + } } async getEmployeePayrollRuns( @@ -46,7 +132,126 @@ export class EmployeePayrollRunService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedHrisEmployeepayrollrunOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const employeePayrollRuns = + await this.prisma.hris_employee_payroll_runs.findMany({ + take: limit + 1, + cursor: cursor ? { id_hris_employee_payroll_run: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + include: { + hris_employee_payroll_runs_deductions: true, + hris_employee_payroll_runs_earnings: true, + hris_employee_payroll_runs_taxes: true, + }, + }); + + const hasNextPage = employeePayrollRuns.length > limit; + if (hasNextPage) employeePayrollRuns.pop(); + + const unifiedEmployeePayrollRuns = await Promise.all( + employeePayrollRuns.map(async (employeePayrollRun) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: + employeePayrollRun.id_hris_employee_payroll_run, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedEmployeePayrollRun: UnifiedHrisEmployeepayrollrunOutput = + { + id: employeePayrollRun.id_hris_employee_payroll_run, + employee_id: employeePayrollRun.id_hris_employee, + payroll_run_id: employeePayrollRun.id_hris_payroll_run, + gross_pay: Number(employeePayrollRun.gross_pay), + net_pay: Number(employeePayrollRun.net_pay), + start_date: employeePayrollRun.start_date, + end_date: employeePayrollRun.end_date, + check_date: employeePayrollRun.check_date, + deductions: + employeePayrollRun.hris_employee_payroll_runs_deductions.map( + (d) => ({ + name: d.name, + employee_deduction: Number(d.employee_deduction), + company_deduction: Number(d.company_deduction), + }), + ), + earnings: + employeePayrollRun.hris_employee_payroll_runs_earnings.map( + (e) => ({ + amount: Number(e.amount), + type: e.type, + }), + ), + taxes: employeePayrollRun.hris_employee_payroll_runs_taxes.map( + (t) => ({ + name: t.name, + amount: Number(t.amount), + employer_tax: t.employer_tax, + }), + ), + field_mappings: field_mappings, + remote_id: employeePayrollRun.remote_id, + remote_created_at: employeePayrollRun.remote_created_at, + created_at: employeePayrollRun.created_at, + modified_at: employeePayrollRun.modified_at, + remote_was_deleted: employeePayrollRun.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: + employeePayrollRun.id_hris_employee_payroll_run, + }, + }); + unifiedEmployeePayrollRun.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedEmployeePayrollRun; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.employee_payroll_run.pull', + method: 'GET', + url: '/hris/employee_payroll_runs', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedEmployeePayrollRuns, + next_cursor: hasNextPage + ? employeePayrollRuns[employeePayrollRuns.length - 1] + .id_hris_employee_payroll_run + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/hris/employeepayrollrun/sync/sync.processor.ts b/packages/api/src/hris/employeepayrollrun/sync/sync.processor.ts new file mode 100644 index 000000000..445b661fd --- /dev/null +++ b/packages/api/src/hris/employeepayrollrun/sync/sync.processor.ts @@ -0,0 +1,21 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; +import { Queues } from '@@core/@core-services/queues/types'; + +@Processor(Queues.SYNC_JOBS_WORKER) +export class SyncProcessor { + constructor(private syncService: SyncService) {} + + @Process('hris-sync-employeepayrollruns') + async handleSyncEmployeePayrollRuns(job: Job) { + try { + console.log( + `Processing queue -> hris-sync-employeepayrollruns ${job.id}`, + ); + await this.syncService.kickstartSync(); + } catch (error) { + console.error('Error syncing hris employee payroll runs', error); + } + } +} diff --git a/packages/api/src/hris/employeepayrollrun/sync/sync.service.ts b/packages/api/src/hris/employeepayrollrun/sync/sync.service.ts index 59f56dc72..a10ce7a70 100644 --- a/packages/api/src/hris/employeepayrollrun/sync/sync.service.ts +++ b/packages/api/src/hris/employeepayrollrun/sync/sync.service.ts @@ -10,6 +10,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedHrisEmployeepayrollrunOutput } from '../types/model.unified'; import { IEmployeePayrollRunService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { HRIS_PROVIDERS } from '@panora/shared'; +import { hris_employee_payroll_runs as HrisEmployeePayrollRun } from '@prisma/client'; +import { OriginalEmployeePayrollRunOutput } from '@@core/utils/types/original/original.hris'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -19,23 +25,192 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('hris', 'employeepayrollrun', this); } - saveToDb( + + async onModuleInit() { + // Initialization logic if needed + } + + @Cron('0 */12 * * *') // every 12 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing employee payroll runs...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of HRIS_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IEmployeePayrollRunService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedHrisEmployeepayrollrunOutput, + OriginalEmployeePayrollRunOutput, + IEmployeePayrollRunService + >(integrationId, linkedUserId, 'hris', 'employeepayrollrun', service, []); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + employeePayrollRuns: UnifiedHrisEmployeepayrollrunOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); + ): Promise { + try { + const employeePayrollRunResults: HrisEmployeePayrollRun[] = []; + + for (let i = 0; i < employeePayrollRuns.length; i++) { + const employeePayrollRun = employeePayrollRuns[i]; + const originId = employeePayrollRun.remote_id; + + let existingEmployeePayrollRun = + await this.prisma.hris_employee_payroll_runs.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const employeePayrollRunData = { + id_hris_employee: employeePayrollRun.employee_id, + id_hris_payroll_run: employeePayrollRun.payroll_run_id, + gross_pay: employeePayrollRun.gross_pay + ? BigInt(employeePayrollRun.gross_pay) + : null, + net_pay: employeePayrollRun.net_pay + ? BigInt(employeePayrollRun.net_pay) + : null, + start_date: employeePayrollRun.start_date + ? new Date(employeePayrollRun.start_date) + : null, + end_date: employeePayrollRun.end_date + ? new Date(employeePayrollRun.end_date) + : null, + check_date: employeePayrollRun.check_date + ? new Date(employeePayrollRun.check_date) + : null, + remote_id: originId, + remote_created_at: employeePayrollRun.remote_created_at + ? new Date(employeePayrollRun.remote_created_at) + : null, + modified_at: new Date(), + remote_was_deleted: employeePayrollRun.remote_was_deleted || false, + }; + + if (existingEmployeePayrollRun) { + existingEmployeePayrollRun = + await this.prisma.hris_employee_payroll_runs.update({ + where: { + id_hris_employee_payroll_run: + existingEmployeePayrollRun.id_hris_employee_payroll_run, + }, + data: employeePayrollRunData, + }); + } else { + existingEmployeePayrollRun = + await this.prisma.hris_employee_payroll_runs.create({ + data: { + ...employeePayrollRunData, + id_hris_employee_payroll_run: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + employeePayrollRunResults.push(existingEmployeePayrollRun); + + // Process field mappings + await this.ingestService.processFieldMappings( + employeePayrollRun.field_mappings, + existingEmployeePayrollRun.id_hris_employee_payroll_run, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingEmployeePayrollRun.id_hris_employee_payroll_run, + remote_data[i], + ); + + // Process deductions, earnings, and taxes + await this.processDeductions( + existingEmployeePayrollRun.id_hris_employee_payroll_run, + employeePayrollRun.deductions, + ); + await this.processEarnings( + existingEmployeePayrollRun.id_hris_employee_payroll_run, + employeePayrollRun.earnings, + ); + await this.processTaxes( + existingEmployeePayrollRun.id_hris_employee_payroll_run, + employeePayrollRun.taxes, + ); + } + + return employeePayrollRunResults; + } catch (error) { + throw error; + } } - async onModuleInit() { - // Initialization logic + private async processDeductions( + id_hris_employee_payroll_run: string, + deductions: any[], + ) { + // Implementation for processing deductions + } + + private async processEarnings( + id_hris_employee_payroll_run: string, + earnings: any[], + ) { + // Implementation for processing earnings } - // Additional methods and logic + private async processTaxes( + id_hris_employee_payroll_run: string, + taxes: any[], + ) { + // Implementation for processing taxes + } } diff --git a/packages/api/src/hris/employeepayrollrun/types/index.ts b/packages/api/src/hris/employeepayrollrun/types/index.ts index 79d61d703..71ef9f61e 100644 --- a/packages/api/src/hris/employeepayrollrun/types/index.ts +++ b/packages/api/src/hris/employeepayrollrun/types/index.ts @@ -5,16 +5,11 @@ import { } from './model.unified'; import { OriginalEmployeePayrollRunOutput } from '@@core/utils/types/original/original.hris'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IEmployeePayrollRunService { - addEmployeePayrollRun( - employeepayrollrunData: DesunifyReturnType, - linkedUserId: string, - ): Promise>; - - syncEmployeePayrollRuns( - linkedUserId: string, - custom_properties?: string[], + sync( + data: SyncParam, ): Promise>; } diff --git a/packages/api/src/hris/employeepayrollrun/types/model.unified.ts b/packages/api/src/hris/employeepayrollrun/types/model.unified.ts index b32914ba1..6d41b69b9 100644 --- a/packages/api/src/hris/employeepayrollrun/types/model.unified.ts +++ b/packages/api/src/hris/employeepayrollrun/types/model.unified.ts @@ -1,3 +1,287 @@ -export class UnifiedHrisEmployeepayrollrunInput {} +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsNumber, + IsDateString, + IsBoolean, + IsArray, +} from 'class-validator'; -export class UnifiedHrisEmployeepayrollrunOutput extends UnifiedHrisEmployeepayrollrunInput {} +class DeductionItem { + @ApiPropertyOptional({ + type: String, + example: 'Health Insurance', + nullable: true, + description: 'The name of the deduction', + }) + @IsString() + @IsOptional() + name?: string; + + @ApiPropertyOptional({ + type: Number, + example: 100, + nullable: true, + description: 'The amount of employee deduction', + }) + @IsNumber() + @IsOptional() + employee_deduction?: number; + + @ApiPropertyOptional({ + type: Number, + example: 200, + nullable: true, + description: 'The amount of company deduction', + }) + @IsNumber() + @IsOptional() + company_deduction?: number; +} + +class EarningItem { + @ApiPropertyOptional({ + type: Number, + example: 1000, + nullable: true, + description: 'The amount of the earning', + }) + @IsNumber() + @IsOptional() + amount?: number; + + @ApiPropertyOptional({ + type: String, + example: 'Salary', + nullable: true, + description: 'The type of the earning', + }) + @IsString() + @IsOptional() + type?: string; +} + +class TaxItem { + @ApiPropertyOptional({ + type: String, + example: 'Federal Income Tax', + nullable: true, + description: 'The name of the tax', + }) + @IsString() + @IsOptional() + name?: string; + + @ApiPropertyOptional({ + type: Number, + example: 250, + nullable: true, + description: 'The amount of the tax', + }) + @IsNumber() + @IsOptional() + amount?: number; + + @ApiPropertyOptional({ + type: Boolean, + example: true, + nullable: true, + description: 'Indicates if this is an employer tax', + }) + @IsBoolean() + @IsOptional() + employer_tax?: boolean; +} + +export class UnifiedHrisEmployeepayrollrunInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated employee', + }) + @IsUUID() + @IsOptional() + employee_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated payroll run', + }) + @IsUUID() + @IsOptional() + payroll_run_id?: string; + + @ApiPropertyOptional({ + type: Number, + example: 5000, + nullable: true, + description: 'The gross pay amount', + }) + @IsNumber() + @IsOptional() + gross_pay?: number; + + @ApiPropertyOptional({ + type: Number, + example: 4000, + nullable: true, + description: 'The net pay amount', + }) + @IsNumber() + @IsOptional() + net_pay?: number; + + @ApiPropertyOptional({ + type: Date, + example: '2023-01-01T00:00:00Z', + nullable: true, + description: 'The start date of the pay period', + }) + @IsDateString() + @IsOptional() + start_date?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2023-01-15T23:59:59Z', + nullable: true, + description: 'The end date of the pay period', + }) + @IsDateString() + @IsOptional() + end_date?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2023-01-20T00:00:00Z', + nullable: true, + description: 'The date the check was issued', + }) + @IsDateString() + @IsOptional() + check_date?: Date; + + @ApiPropertyOptional({ + type: [DeductionItem], + nullable: true, + description: 'The list of deductions for this payroll run', + }) + @IsArray() + @IsOptional() + deductions?: DeductionItem[]; + + @ApiPropertyOptional({ + type: [EarningItem], + nullable: true, + description: 'The list of earnings for this payroll run', + }) + @IsArray() + @IsOptional() + earnings?: EarningItem[]; + + @ApiPropertyOptional({ + type: [TaxItem], + nullable: true, + description: 'The list of taxes for this payroll run', + }) + @IsArray() + @IsOptional() + taxes?: TaxItem[]; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedHrisEmployeepayrollrunOutput extends UnifiedHrisEmployeepayrollrunInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the employee payroll run record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'payroll_run_1234', + nullable: true, + description: + 'The remote ID of the employee payroll run in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the employee payroll run in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: + 'The date when the employee payroll run was created in the 3rd party system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The created date of the employee payroll run record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The last modified date of the employee payroll run record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; + + @ApiPropertyOptional({ + type: Boolean, + example: false, + nullable: true, + description: + 'Indicates if the employee payroll run was deleted in the remote system', + }) + @IsBoolean() + @IsOptional() + remote_was_deleted?: boolean; +} diff --git a/packages/api/src/hris/employerbenefit/employerbenefit.controller.ts b/packages/api/src/hris/employerbenefit/employerbenefit.controller.ts index c4a5a6eec..d0067f6cc 100644 --- a/packages/api/src/hris/employerbenefit/employerbenefit.controller.ts +++ b/packages/api/src/hris/employerbenefit/employerbenefit.controller.ts @@ -1,38 +1,31 @@ +import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { Controller, - Post, - Body, - Query, Get, - Patch, - Param, Headers, + Param, + Query, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { - ApiBody, + ApiHeader, ApiOperation, ApiParam, ApiQuery, ApiTags, - ApiHeader, - //ApiKeyAuth, } from '@nestjs/swagger'; -import { EmployerBenefitService } from './services/employerbenefit.service'; -import { - UnifiedHrisEmployerbenefitInput, - UnifiedHrisEmployerbenefitOutput, -} from './types/model.unified'; -import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; -import { QueryDto } from '@@core/utils/dtos/query.dto'; +import { ConnectionUtils } from '@@core/connections/@utils'; import { ApiGetCustomResponse, ApiPaginatedResponse, } from '@@core/utils/dtos/openapi.respone.dto'; - +import { QueryDto } from '@@core/utils/dtos/query.dto'; +import { EmployerBenefitService } from './services/employerbenefit.service'; +import { UnifiedHrisEmployerbenefitOutput } from './types/model.unified'; @ApiTags('hris/employerbenefits') @Controller('hris/employerbenefits') @@ -57,6 +50,7 @@ export class EmployerBenefitController { }) @ApiPaginatedResponse(UnifiedHrisEmployerbenefitOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getEmployerBenefits( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/hris/employerbenefit/employerbenefit.module.ts b/packages/api/src/hris/employerbenefit/employerbenefit.module.ts index 5d178e717..29a62ed01 100644 --- a/packages/api/src/hris/employerbenefit/employerbenefit.module.ts +++ b/packages/api/src/hris/employerbenefit/employerbenefit.module.ts @@ -1,34 +1,27 @@ import { Module } from '@nestjs/common'; import { EmployerBenefitController } from './employerbenefit.controller'; -import { SyncService } from './sync/sync.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { EmployerBenefitService } from './services/employerbenefit.service'; import { ServiceRegistry } from './services/registry.service'; -import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; - -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { BullModule } from '@nestjs/bull'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { SyncService } from './sync/sync.service'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; - +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; - +import { GustoEmployerbenefitMapper } from './services/gusto/mappers'; +import { GustoService } from './services/gusto'; +import { Utils } from '@hris/@lib/@utils'; @Module({ controllers: [EmployerBenefitController], providers: [ EmployerBenefitService, CoreUnification, - SyncService, WebhookService, - ServiceRegistry, - + Utils, IngestDataService, + GustoEmployerbenefitMapper, /* PROVIDERS SERVICES */ + GustoService, ], exports: [SyncService], }) diff --git a/packages/api/src/hris/employerbenefit/services/employerbenefit.service.ts b/packages/api/src/hris/employerbenefit/services/employerbenefit.service.ts index 4f90ecd6c..fedd3cb88 100644 --- a/packages/api/src/hris/employerbenefit/services/employerbenefit.service.ts +++ b/packages/api/src/hris/employerbenefit/services/employerbenefit.service.ts @@ -9,12 +9,8 @@ import { UnifiedHrisEmployerbenefitInput, UnifiedHrisEmployerbenefitOutput, } from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; -import { OriginalEmployerBenefitOutput } from '@@core/utils/types/original/original.hris'; - -import { IEmployerBenefitService } from '../types'; @Injectable() export class EmployerBenefitService { @@ -29,14 +25,83 @@ export class EmployerBenefitService { } async getEmployerBenefit( - id_employerbenefiting_employerbenefit: string, + id_hris_employer_benefit: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const employerBenefit = + await this.prisma.hris_employer_benefits.findUnique({ + where: { id_hris_employer_benefit: id_hris_employer_benefit }, + }); + + if (!employerBenefit) { + throw new Error( + `Employer Benefit with ID ${id_hris_employer_benefit} not found.`, + ); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: employerBenefit.id_hris_employer_benefit, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedEmployerBenefit: UnifiedHrisEmployerbenefitOutput = { + id: employerBenefit.id_hris_employer_benefit, + benefit_plan_type: employerBenefit.benefit_plan_type, + name: employerBenefit.name, + description: employerBenefit.description, + deduction_code: employerBenefit.deduction_code, + field_mappings: field_mappings, + remote_id: employerBenefit.remote_id, + remote_created_at: employerBenefit.remote_created_at, + created_at: employerBenefit.created_at, + modified_at: employerBenefit.modified_at, + remote_was_deleted: employerBenefit.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: employerBenefit.id_hris_employer_benefit, + }, + }); + unifiedEmployerBenefit.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.employer_benefit.pull', + method: 'GET', + url: '/hris/employer_benefit', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedEmployerBenefit; + } catch (error) { + throw error; + } } async getEmployerBenefits( @@ -47,7 +112,93 @@ export class EmployerBenefitService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedHrisEmployerbenefitOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const employerBenefits = + await this.prisma.hris_employer_benefits.findMany({ + take: limit + 1, + cursor: cursor ? { id_hris_employer_benefit: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = employerBenefits.length > limit; + if (hasNextPage) employerBenefits.pop(); + + const unifiedEmployerBenefits = await Promise.all( + employerBenefits.map(async (employerBenefit) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: employerBenefit.id_hris_employer_benefit, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedEmployerBenefit: UnifiedHrisEmployerbenefitOutput = { + id: employerBenefit.id_hris_employer_benefit, + benefit_plan_type: employerBenefit.benefit_plan_type, + name: employerBenefit.name, + description: employerBenefit.description, + deduction_code: employerBenefit.deduction_code, + field_mappings: field_mappings, + remote_id: employerBenefit.remote_id, + remote_created_at: employerBenefit.remote_created_at, + created_at: employerBenefit.created_at, + modified_at: employerBenefit.modified_at, + remote_was_deleted: employerBenefit.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: employerBenefit.id_hris_employer_benefit, + }, + }); + unifiedEmployerBenefit.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedEmployerBenefit; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.employer_benefit.pull', + method: 'GET', + url: '/hris/employer_benefits', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedEmployerBenefits, + next_cursor: hasNextPage + ? employerBenefits[employerBenefits.length - 1] + .id_hris_employer_benefit + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/hris/employerbenefit/services/gusto/index.ts b/packages/api/src/hris/employerbenefit/services/gusto/index.ts new file mode 100644 index 000000000..2024d7b4a --- /dev/null +++ b/packages/api/src/hris/employerbenefit/services/gusto/index.ts @@ -0,0 +1,96 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { HrisObject } from '@hris/@lib/@types'; +import { IEmployerBenefitService } from '@hris/employerbenefit/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { GustoEmployerbenefitOutput } from './types'; + +@Injectable() +export class GustoService implements IEmployerBenefitService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + HrisObject.employerbenefit.toUpperCase() + ':' + GustoService.name, + ); + this.registry.registerService('gusto', this); + } + + async sync( + data: SyncParam, + ): Promise> { + try { + const { linkedUserId, id_company } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'gusto', + vertical: 'hris', + }, + }); + + const company = await this.prisma.hris_companies.findUnique({ + where: { + id_hris_company: id_company as string, + }, + select: { + remote_id: true, + }, + }); + + const resp = await axios.get( + `${connection.account_url}/v1/companies/${company.remote_id}/company_benefits`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + + const resp_ = await axios.get(`${connection.account_url}/v1/benefits`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + + const res = []; + for (const employerBenefit of resp.data) { + const pick = resp_.data.filter( + (item) => item.benefit_type == employerBenefit.benefit_type, + ); + res.push({ + ...employerBenefit, + category: pick.category, + name: pick.name, + }); + } + + this.logger.log(`Synced gusto employerbenefits !`); + + return { + data: res, + message: 'Gusto employerbenefits retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/hris/employerbenefit/services/gusto/mappers.ts b/packages/api/src/hris/employerbenefit/services/gusto/mappers.ts new file mode 100644 index 000000000..3d2c4d07f --- /dev/null +++ b/packages/api/src/hris/employerbenefit/services/gusto/mappers.ts @@ -0,0 +1,90 @@ +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { Injectable } from '@nestjs/common'; +import { GustoCategory, GustoEmployerbenefitOutput } from './types'; +import { + BenefitPlanType, + UnifiedHrisEmployerbenefitInput, + UnifiedHrisEmployerbenefitOutput, +} from '@hris/employerbenefit/types/model.unified'; +import { IEmployerBenefitMapper } from '@hris/employerbenefit/types'; +import { Utils } from '@hris/@lib/@utils'; + +@Injectable() +export class GustoEmployerbenefitMapper implements IEmployerBenefitMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService( + 'hris', + 'employerbenefit', + 'gusto', + this, + ); + } + + async desunify( + source: UnifiedHrisEmployerbenefitInput, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return; + } + + async unify( + source: GustoEmployerbenefitOutput | GustoEmployerbenefitOutput[], + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise< + UnifiedHrisEmployerbenefitOutput | UnifiedHrisEmployerbenefitOutput[] + > { + if (!Array.isArray(source)) { + return this.mapSingleEmployerbenefitToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((employerbenefit) => + this.mapSingleEmployerbenefitToUnified( + employerbenefit, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleEmployerbenefitToUnified( + employerbenefit: GustoEmployerbenefitOutput, + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return { + remote_id: employerbenefit.uuid || null, + remote_data: employerbenefit, + benefit_plan_type: this.mapGustoBenefitToPanora(employerbenefit.category), + name: employerbenefit.name, + description: employerbenefit.description, + }; + } + + mapGustoBenefitToPanora( + category: GustoCategory | string, + ): BenefitPlanType | string { + switch (category) { + case 'Health': + return 'MEDICAL'; + case 'Savings and Retirement': + return 'RETIREMENT'; + case 'Other': + return 'OTHER'; + default: + return category; + } + } +} diff --git a/packages/api/src/hris/employerbenefit/services/gusto/types.ts b/packages/api/src/hris/employerbenefit/services/gusto/types.ts new file mode 100644 index 000000000..6834a7094 --- /dev/null +++ b/packages/api/src/hris/employerbenefit/services/gusto/types.ts @@ -0,0 +1,20 @@ +export type GustoEmployerbenefitOutput = Partial<{ + uuid: string; + version: string; + company_uuid: string; + benefit_type: number; + active: boolean; + description: string; + deletable: boolean; + supports_percentage_amounts: boolean; + responsible_for_employer_taxes: boolean; + responsible_for_employee_w2: boolean; + category: string; + name: string; +}>; + +export type GustoCategory = + | 'Health' + | 'Savings and Retirement' + | 'Transportation' + | 'Other'; diff --git a/packages/api/src/hris/employerbenefit/sync/sync.processor.ts b/packages/api/src/hris/employerbenefit/sync/sync.processor.ts new file mode 100644 index 000000000..b42fed452 --- /dev/null +++ b/packages/api/src/hris/employerbenefit/sync/sync.processor.ts @@ -0,0 +1,19 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; +import { Queues } from '@@core/@core-services/queues/types'; + +@Processor(Queues.SYNC_JOBS_WORKER) +export class SyncProcessor { + constructor(private syncService: SyncService) {} + + @Process('hris-sync-employerbenefits') + async handleSyncEmployerBenefits(job: Job) { + try { + console.log(`Processing queue -> hris-sync-employerbenefits ${job.id}`); + await this.syncService.kickstartSync(); + } catch (error) { + console.error('Error syncing hris employer benefits', error); + } + } +} diff --git a/packages/api/src/hris/employerbenefit/sync/sync.service.ts b/packages/api/src/hris/employerbenefit/sync/sync.service.ts index 9953cb218..ed021da8f 100644 --- a/packages/api/src/hris/employerbenefit/sync/sync.service.ts +++ b/packages/api/src/hris/employerbenefit/sync/sync.service.ts @@ -10,6 +10,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedHrisEmployerbenefitOutput } from '../types/model.unified'; import { IEmployerBenefitService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { HRIS_PROVIDERS } from '@panora/shared'; +import { hris_employer_benefits as HrisEmployerBenefit } from '@prisma/client'; +import { OriginalEmployerBenefitOutput } from '@@core/utils/types/original/original.hris'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -19,23 +25,151 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('hris', 'employerbenefit', this); } - saveToDb( + + async onModuleInit() { + // Initialization logic if needed + } + + @Cron('0 */12 * * *') // every 12 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing employer benefits...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of HRIS_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId, id_company } = param; + const service: IEmployerBenefitService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedHrisEmployerbenefitOutput, + OriginalEmployerBenefitOutput, + IEmployerBenefitService + >(integrationId, linkedUserId, 'hris', 'employerbenefit', service, [ + { + param: id_company, + paramName: 'id_company', + shouldPassToService: true, + shouldPassToIngest: true, + }, + ]); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + employerBenefits: UnifiedHrisEmployerbenefitOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const employerBenefitResults: HrisEmployerBenefit[] = []; - async onModuleInit() { - // Initialization logic - } + for (let i = 0; i < employerBenefits.length; i++) { + const employerBenefit = employerBenefits[i]; + const originId = employerBenefit.remote_id; + + let existingEmployerBenefit = + await this.prisma.hris_employer_benefits.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const employerBenefitData = { + benefit_plan_type: employerBenefit.benefit_plan_type, + name: employerBenefit.name, + description: employerBenefit.description, + deduction_code: employerBenefit.deduction_code, + remote_id: originId, + remote_created_at: employerBenefit.remote_created_at + ? new Date(employerBenefit.remote_created_at) + : null, + modified_at: new Date(), + remote_was_deleted: employerBenefit.remote_was_deleted || false, + }; - // Additional methods and logic + if (existingEmployerBenefit) { + existingEmployerBenefit = + await this.prisma.hris_employer_benefits.update({ + where: { + id_hris_employer_benefit: + existingEmployerBenefit.id_hris_employer_benefit, + }, + data: employerBenefitData, + }); + } else { + existingEmployerBenefit = + await this.prisma.hris_employer_benefits.create({ + data: { + ...employerBenefitData, + id_hris_employer_benefit: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + employerBenefitResults.push(existingEmployerBenefit); + + // Process field mappings + await this.ingestService.processFieldMappings( + employerBenefit.field_mappings, + existingEmployerBenefit.id_hris_employer_benefit, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingEmployerBenefit.id_hris_employer_benefit, + remote_data[i], + ); + } + + return employerBenefitResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/hris/employerbenefit/types/index.ts b/packages/api/src/hris/employerbenefit/types/index.ts index bf453af84..af9533a28 100644 --- a/packages/api/src/hris/employerbenefit/types/index.ts +++ b/packages/api/src/hris/employerbenefit/types/index.ts @@ -5,17 +5,10 @@ import { } from './model.unified'; import { OriginalEmployerBenefitOutput } from '@@core/utils/types/original/original.hris'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IEmployerBenefitService { - addEmployerBenefit( - employerbenefitData: DesunifyReturnType, - linkedUserId: string, - ): Promise>; - - syncEmployerBenefits( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IEmployerBenefitMapper { @@ -34,5 +27,7 @@ export interface IEmployerBenefitMapper { slug: string; remote_id: string; }[], - ): Promise; + ): Promise< + UnifiedHrisEmployerbenefitOutput | UnifiedHrisEmployerbenefitOutput[] + >; } diff --git a/packages/api/src/hris/employerbenefit/types/model.unified.ts b/packages/api/src/hris/employerbenefit/types/model.unified.ts index f5949144c..3f280edb6 100644 --- a/packages/api/src/hris/employerbenefit/types/model.unified.ts +++ b/packages/api/src/hris/employerbenefit/types/model.unified.ts @@ -1,3 +1,149 @@ -export class UnifiedHrisEmployerbenefitInput {} +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsBoolean, + IsDateString, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; -export class UnifiedHrisEmployerbenefitOutput extends UnifiedHrisEmployerbenefitInput {} +export type BenefitPlanType = + | 'MEDICAL' + | 'HEALTH_SAVINGS' + | 'INSURANCE' + | 'RETIREMENT' + | 'OTHER'; +export class UnifiedHrisEmployerbenefitInput { + @ApiPropertyOptional({ + type: String, + example: 'Health Insurance', + enum: ['MEDICAL', 'HEALTH_SAVINGS', 'INSURANCE', 'RETIREMENT', 'OTHER'], + nullable: true, + description: 'The type of the benefit plan', + }) + @IsString() + @IsOptional() + benefit_plan_type?: BenefitPlanType | string; + + @ApiPropertyOptional({ + type: String, + example: 'Company Health Plan', + nullable: true, + description: 'The name of the employer benefit', + }) + @IsString() + @IsOptional() + name?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Comprehensive health insurance coverage for employees', + nullable: true, + description: 'The description of the employer benefit', + }) + @IsString() + @IsOptional() + description?: string; + + @ApiPropertyOptional({ + type: String, + example: 'HEALTH-001', + nullable: true, + description: 'The deduction code for the employer benefit', + }) + @IsString() + @IsOptional() + deduction_code?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedHrisEmployerbenefitOutput extends UnifiedHrisEmployerbenefitInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the employer benefit record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'benefit_1234', + nullable: true, + description: + 'The remote ID of the employer benefit in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the employer benefit in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: + 'The date when the employer benefit was created in the 3rd party system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The created date of the employer benefit record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The last modified date of the employer benefit record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; + + @ApiPropertyOptional({ + type: Boolean, + example: false, + nullable: true, + description: + 'Indicates if the employer benefit was deleted in the remote system', + }) + @IsBoolean() + @IsOptional() + remote_was_deleted?: boolean; +} diff --git a/packages/api/src/hris/employment/employment.controller.ts b/packages/api/src/hris/employment/employment.controller.ts index 3624d807f..3c8a010b7 100644 --- a/packages/api/src/hris/employment/employment.controller.ts +++ b/packages/api/src/hris/employment/employment.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -56,6 +58,7 @@ export class EmploymentController { }) @ApiPaginatedResponse(UnifiedHrisEmploymentOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getEmployments( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/hris/employment/employment.module.ts b/packages/api/src/hris/employment/employment.module.ts index 0748b4d4a..e5d64d523 100644 --- a/packages/api/src/hris/employment/employment.module.ts +++ b/packages/api/src/hris/employment/employment.module.ts @@ -1,33 +1,24 @@ import { Module } from '@nestjs/common'; import { EmploymentController } from './employment.controller'; -import { SyncService } from './sync/sync.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { EmploymentService } from './services/employment.service'; import { ServiceRegistry } from './services/registry.service'; -import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; - -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { BullModule } from '@nestjs/bull'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { SyncService } from './sync/sync.service'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; - +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; - +import { GustoEmploymentMapper } from './services/gusto/mappers'; +import { Utils } from '@hris/@lib/@utils'; @Module({ controllers: [EmploymentController], providers: [ EmploymentService, CoreUnification, - SyncService, WebhookService, - ServiceRegistry, - IngestDataService, + Utils, + GustoEmploymentMapper, /* PROVIDERS SERVICES */ ], exports: [SyncService], diff --git a/packages/api/src/hris/employment/services/employment.service.ts b/packages/api/src/hris/employment/services/employment.service.ts index 18f290524..ba0ab6d81 100644 --- a/packages/api/src/hris/employment/services/employment.service.ts +++ b/packages/api/src/hris/employment/services/employment.service.ts @@ -1,20 +1,12 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { - UnifiedHrisEmploymentInput, - UnifiedHrisEmploymentOutput, -} from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; +import { UnifiedHrisEmploymentOutput } from '../types/model.unified'; import { ServiceRegistry } from './registry.service'; -import { OriginalEmploymentOutput } from '@@core/utils/types/original/original.hris'; - -import { IEmploymentService } from '../types'; +import { CurrencyCode } from '@@core/utils/types'; @Injectable() export class EmploymentService { @@ -29,14 +21,84 @@ export class EmploymentService { } async getEmployment( - id_employmenting_employment: string, + id_hris_employment: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const employment = await this.prisma.hris_employments.findUnique({ + where: { id_hris_employment: id_hris_employment }, + }); + + if (!employment) { + throw new Error(`Employment with ID ${id_hris_employment} not found.`); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: employment.id_hris_employment, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedEmployment: UnifiedHrisEmploymentOutput = { + id: employment.id_hris_employment, + job_title: employment.job_title, + pay_rate: Number(employment.pay_rate), + pay_period: employment.pay_period, + pay_frequency: employment.pay_frequency, + pay_currency: employment.pay_currency as CurrencyCode, + flsa_status: employment.flsa_status, + effective_date: employment.effective_date, + employment_type: employment.employment_type, + field_mappings: field_mappings, + remote_id: employment.remote_id, + remote_created_at: employment.remote_created_at, + created_at: employment.created_at, + modified_at: employment.modified_at, + remote_was_deleted: employment.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: employment.id_hris_employment, + }, + }); + unifiedEmployment.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.employment.pull', + method: 'GET', + url: '/hris/employment', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedEmployment; + } catch (error) { + throw error; + } } async getEmployments( @@ -47,7 +109,95 @@ export class EmploymentService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedHrisEmploymentOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const employments = await this.prisma.hris_employments.findMany({ + take: limit + 1, + cursor: cursor ? { id_hris_employment: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = employments.length > limit; + if (hasNextPage) employments.pop(); + + const unifiedEmployments = await Promise.all( + employments.map(async (employment) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: employment.id_hris_employment, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedEmployment: UnifiedHrisEmploymentOutput = { + id: employment.id_hris_employment, + job_title: employment.job_title, + pay_rate: Number(employment.pay_rate), + pay_period: employment.pay_period, + pay_frequency: employment.pay_frequency, + pay_currency: employment.pay_currency as CurrencyCode, + flsa_status: employment.flsa_status, + effective_date: employment.effective_date, + employment_type: employment.employment_type, + field_mappings: field_mappings, + remote_id: employment.remote_id, + remote_created_at: employment.remote_created_at, + created_at: employment.created_at, + modified_at: employment.modified_at, + remote_was_deleted: employment.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: employment.id_hris_employment, + }, + }); + unifiedEmployment.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedEmployment; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.employment.pull', + method: 'GET', + url: '/hris/employments', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedEmployments, + next_cursor: hasNextPage + ? employments[employments.length - 1].id_hris_employment + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/hris/employment/services/gusto/mappers.ts b/packages/api/src/hris/employment/services/gusto/mappers.ts new file mode 100644 index 000000000..af9fd3366 --- /dev/null +++ b/packages/api/src/hris/employment/services/gusto/mappers.ts @@ -0,0 +1,92 @@ +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { Utils } from '@hris/@lib/@utils'; +import { IEmploymentMapper } from '@hris/employment/types'; +import { + FlsaStatus, + UnifiedHrisEmploymentInput, + UnifiedHrisEmploymentOutput, +} from '@hris/employment/types/model.unified'; +import { Injectable } from '@nestjs/common'; +import { GustoEmploymentOutput } from './types'; + +@Injectable() +export class GustoEmploymentMapper implements IEmploymentMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService('hris', 'employment', 'gusto', this); + } + + async desunify( + source: UnifiedHrisEmploymentInput, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return; + } + + async unify( + source: GustoEmploymentOutput | GustoEmploymentOutput[], + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleEmploymentToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((employment) => + this.mapSingleEmploymentToUnified( + employment, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleEmploymentToUnified( + employment: GustoEmploymentOutput, + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return { + remote_id: employment.uuid, + remote_data: employment, + effective_date: new Date(employment.effective_date), + job_title: employment.title, + pay_rate: Number(employment.rate), + flsa_status: this.mapFlsaStatusToPanora(employment.flsa_status), + }; + } + + mapFlsaStatusToPanora( + str: + | 'Exempt' + | 'Salaried Nonexempt' + | 'Nonexempt' + | 'Owner' + | 'Commission Only Exempt' + | 'Commission Only Nonexempt', + ): FlsaStatus | string { + switch (str) { + case 'Exempt': + return 'EXEMPT'; + case 'Salaried Nonexempt': + return 'SALARIED_NONEXEMPT'; + case 'Nonexempt': + return 'NONEXEMPT'; + case 'Owner': + return 'OWNER'; + default: + return str; + } + } +} diff --git a/packages/api/src/hris/employment/services/gusto/types.ts b/packages/api/src/hris/employment/services/gusto/types.ts new file mode 100644 index 000000000..9cc0b0112 --- /dev/null +++ b/packages/api/src/hris/employment/services/gusto/types.ts @@ -0,0 +1,31 @@ +export type GustoEmploymentOutput = Partial<{ + uuid: string; // The UUID of the compensation in Gusto. + version: string; // The current version of the compensation. + job_uuid: string; // The UUID of the job to which the compensation belongs. + title: string; + rate: string; // The dollar amount paid per payment unit. + payment_unit: 'Hour' | 'Week' | 'Month' | 'Year' | 'Paycheck'; // The unit accompanying the compensation rate. + flsa_status: + | 'Exempt' + | 'Salaried Nonexempt' + | 'Nonexempt' + | 'Owner' + | 'Commission Only Exempt' + | 'Commission Only Nonexempt'; // The FLSA status for this compensation. + effective_date: string; // The effective date for this compensation. + adjust_for_minimum_wage: boolean; // Indicates if the compensation could be adjusted to minimum wage during payroll calculation. + eligible_paid_time_off: EligiblePaidTimeOff[]; // The available types of paid time off for the compensation. +}>; + +type EligiblePaidTimeOff = { + name: string; // The name of the paid time off type. + policy_name: string; // The name of the time off policy. + policy_uuid: string; // The UUID of the time off policy. + accrual_unit: string; // The unit the PTO type is accrued in. + accrual_rate: string; // The number of accrual units accrued per accrual period. + accrual_method: string; // The accrual method of the time off policy. + accrual_period: string; // The frequency at which the PTO type is accrued. + accrual_balance: string; // The number of accrual units accrued. + maximum_accrual_balance: string | null; // The maximum number of accrual units allowed. + paid_at_termination: boolean; // Whether the accrual balance is paid to the employee upon termination. +}; diff --git a/packages/api/src/hris/employment/sync/sync.processor.ts b/packages/api/src/hris/employment/sync/sync.processor.ts new file mode 100644 index 000000000..0f7562dfe --- /dev/null +++ b/packages/api/src/hris/employment/sync/sync.processor.ts @@ -0,0 +1,19 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; +import { Queues } from '@@core/@core-services/queues/types'; + +@Processor(Queues.SYNC_JOBS_WORKER) +export class SyncProcessor { + constructor(private syncService: SyncService) {} + + @Process('hris-sync-employments') + async handleSyncEmployments(job: Job) { + try { + console.log(`Processing queue -> hris-sync-employments ${job.id}`); + await this.syncService.kickstartSync(); + } catch (error) { + console.error('Error syncing hris employments', error); + } + } +} diff --git a/packages/api/src/hris/employment/sync/sync.service.ts b/packages/api/src/hris/employment/sync/sync.service.ts index a43745982..0a383ddb3 100644 --- a/packages/api/src/hris/employment/sync/sync.service.ts +++ b/packages/api/src/hris/employment/sync/sync.service.ts @@ -2,7 +2,6 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; @@ -10,6 +9,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedHrisEmploymentOutput } from '../types/model.unified'; import { IEmploymentService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { HRIS_PROVIDERS } from '@panora/shared'; +import { hris_employments as HrisEmployment } from '@prisma/client'; +import { OriginalEmploymentOutput } from '@@core/utils/types/original/original.hris'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -19,23 +24,146 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('hris', 'employment', this); } - saveToDb( + + async onModuleInit() { + // Initialization logic if needed + } + + @Cron('0 */12 * * *') // every 12 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing employments...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of HRIS_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IEmploymentService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedHrisEmploymentOutput, + OriginalEmploymentOutput, + IEmploymentService + >(integrationId, linkedUserId, 'hris', 'employment', service, []); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + employments: UnifiedHrisEmploymentOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const employmentResults: HrisEmployment[] = []; - async onModuleInit() { - // Initialization logic - } + for (let i = 0; i < employments.length; i++) { + const employment = employments[i]; + const originId = employment.remote_id; + + let existingEmployment = await this.prisma.hris_employments.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const employmentData = { + job_title: employment.job_title, + pay_rate: employment.pay_rate ? BigInt(employment.pay_rate) : null, + pay_period: employment.pay_period, + pay_frequency: employment.pay_frequency, + pay_currency: employment.pay_currency, + flsa_status: employment.flsa_status, + effective_date: employment.effective_date + ? new Date(employment.effective_date) + : null, + employment_type: employment.employment_type, + remote_id: originId, + remote_created_at: employment.remote_created_at + ? new Date(employment.remote_created_at) + : null, + modified_at: new Date(), + remote_was_deleted: employment.remote_was_deleted || false, + }; - // Additional methods and logic + if (existingEmployment) { + existingEmployment = await this.prisma.hris_employments.update({ + where: { + id_hris_employment: existingEmployment.id_hris_employment, + }, + data: employmentData, + }); + } else { + existingEmployment = await this.prisma.hris_employments.create({ + data: { + ...employmentData, + id_hris_employment: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + employmentResults.push(existingEmployment); + + // Process field mappings + await this.ingestService.processFieldMappings( + employment.field_mappings, + existingEmployment.id_hris_employment, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingEmployment.id_hris_employment, + remote_data[i], + ); + } + + return employmentResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/hris/employment/types/index.ts b/packages/api/src/hris/employment/types/index.ts index b88d11e85..3a3ec8bdc 100644 --- a/packages/api/src/hris/employment/types/index.ts +++ b/packages/api/src/hris/employment/types/index.ts @@ -5,17 +5,10 @@ import { } from './model.unified'; import { OriginalEmploymentOutput } from '@@core/utils/types/original/original.hris'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IEmploymentService { - addEmployment( - employmentData: DesunifyReturnType, - linkedUserId: string, - ): Promise>; - - syncEmployments( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IEmploymentMapper { diff --git a/packages/api/src/hris/employment/types/model.unified.ts b/packages/api/src/hris/employment/types/model.unified.ts index 5c936ddb6..aef4d7031 100644 --- a/packages/api/src/hris/employment/types/model.unified.ts +++ b/packages/api/src/hris/employment/types/model.unified.ts @@ -1,3 +1,263 @@ -export class UnifiedHrisEmploymentInput {} +import { CurrencyCode } from '@@core/utils/types'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsNumber, + IsDateString, + IsBoolean, +} from 'class-validator'; -export class UnifiedHrisEmploymentOutput extends UnifiedHrisEmploymentInput {} +export type FlsaStatus = + | 'EXEMPT' + | 'SALARIED_NONEXEMPT' + | 'NONEXEMPT' + | 'OWNER'; + +export type EmploymentType = + | 'FULL_TIME' + | 'PART_TIME' + | 'INTERN' + | 'CONTRACTOR' + | 'FREELANCE'; + +export type PayFrequency = + | 'WEEKLY' + | 'BIWEEKLY' + | 'MONTHLY' + | 'QUARTERLY' + | 'SEMIANNUALLY' + | 'ANNUALLY' + | 'THIRTEEN-MONTHLY' + | 'PRO_RATA' + | 'SEMIMONTHLY'; + +export type PayPeriod = + | 'HOUR' + | 'DAY' + | 'WEEK' + | 'EVERY_TWO_WEEKS' + | 'SEMIMONTHLY' + | 'MONTH' + | 'QUARTER' + | 'EVERY_SIX_MONTHS' + | 'YEAR'; + +export class UnifiedHrisEmploymentInput { + @ApiPropertyOptional({ + type: String, + example: 'Software Engineer', + nullable: true, + description: 'The job title of the employment', + }) + @IsString() + @IsOptional() + job_title?: string; + + @ApiPropertyOptional({ + type: Number, + example: 100000, + nullable: true, + description: 'The pay rate of the employment', + }) + @IsNumber() + @IsOptional() + pay_rate?: number; + + @ApiPropertyOptional({ + type: String, + example: 'MONTHLY', + enum: [ + 'HOUR', + 'DAY', + 'WEEK', + 'EVERY_TWO_WEEKS', + 'SEMIMONTHLY', + 'MONTH', + 'QUARTER', + 'EVERY_SIX_MONTHS', + 'YEAR', + ], + nullable: true, + description: 'The pay period of the employment', + }) + @IsString() + @IsOptional() + pay_period?: PayPeriod | string; + + @ApiPropertyOptional({ + type: String, + example: 'WEEKLY', + enum: [ + 'WEEKLY', + 'BIWEEKLY', + 'MONTHLY', + 'QUARTERLY', + 'SEMIANNUALLY', + 'ANNUALLY', + 'THIRTEEN-MONTHLY', + 'PRO_RATA', + 'SEMIMONTHLY', + ], + nullable: true, + description: 'The pay frequency of the employment', + }) + @IsString() + @IsOptional() + pay_frequency?: PayFrequency | string; + + @ApiPropertyOptional({ + type: String, + example: 'USD', + enum: CurrencyCode, + nullable: true, + description: 'The currency of the pay', + }) + @IsString() + @IsOptional() + pay_currency?: CurrencyCode; + + @ApiPropertyOptional({ + type: String, + example: 'EXEMPT', + enum: ['EXEMPT', 'SALARIED_NONEXEMPT', 'NONEXEMPT', 'OWNER'], + nullable: true, + description: 'The FLSA status of the employment', + }) + @IsString() + @IsOptional() + flsa_status?: FlsaStatus | string; + + @ApiPropertyOptional({ + type: Date, + example: '2023-01-01', + nullable: true, + description: 'The effective date of the employment', + }) + @IsDateString() + @IsOptional() + effective_date?: Date; + + @ApiPropertyOptional({ + type: String, + example: 'FULL_TIME', + enum: ['FULL_TIME', 'PART_TIME', 'INTERN', 'CONTRACTOR', 'FREELANCE'], + nullable: true, + description: 'The type of employment', + }) + @IsString() + @IsOptional() + employment_type?: EmploymentType | string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated pay group', + }) + @IsUUID() + @IsOptional() + pay_group_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated employee', + }) + @IsUUID() + @IsOptional() + employee_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedHrisEmploymentOutput extends UnifiedHrisEmploymentInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the employment record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'employment_1234', + nullable: true, + description: + 'The remote ID of the employment in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the employment in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: + 'The date when the employment was created in the 3rd party system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The created date of the employment record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The last modified date of the employment record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; + + @ApiPropertyOptional({ + type: Boolean, + example: false, + nullable: true, + description: 'Indicates if the employment was deleted in the remote system', + }) + @IsBoolean() + @IsOptional() + remote_was_deleted?: boolean; +} diff --git a/packages/api/src/hris/group/group.controller.ts b/packages/api/src/hris/group/group.controller.ts index 03f83b8a9..73fe0fe09 100644 --- a/packages/api/src/hris/group/group.controller.ts +++ b/packages/api/src/hris/group/group.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -33,7 +35,6 @@ import { ApiPaginatedResponse, } from '@@core/utils/dtos/openapi.respone.dto'; - @ApiTags('hris/groups') @Controller('hris/groups') export class GroupController { @@ -57,6 +58,7 @@ export class GroupController { }) @ApiPaginatedResponse(UnifiedHrisGroupOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getGroups( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/hris/group/group.module.ts b/packages/api/src/hris/group/group.module.ts index 8b6d96546..9e1bdaa36 100644 --- a/packages/api/src/hris/group/group.module.ts +++ b/packages/api/src/hris/group/group.module.ts @@ -1,35 +1,27 @@ import { Module } from '@nestjs/common'; import { GroupController } from './group.controller'; -import { SyncService } from './sync/sync.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { GroupService } from './services/group.service'; import { ServiceRegistry } from './services/registry.service'; -import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; - -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { BullModule } from '@nestjs/bull'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { SyncService } from './sync/sync.service'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; - +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; - +import { GustoGroupMapper } from './services/gusto/mappers'; +import { GustoService } from './services/gusto'; +import { Utils } from '@hris/@lib/@utils'; @Module({ controllers: [GroupController], providers: [ GroupService, - SyncService, WebhookService, - CoreUnification, - ServiceRegistry, - IngestDataService, + GustoGroupMapper, + Utils, /* PROVIDERS SERVICES */ + GustoService, ], exports: [SyncService], }) diff --git a/packages/api/src/hris/group/services/group.service.ts b/packages/api/src/hris/group/services/group.service.ts index b45ae7029..378b9af64 100644 --- a/packages/api/src/hris/group/services/group.service.ts +++ b/packages/api/src/hris/group/services/group.service.ts @@ -2,16 +2,13 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { UnifiedHrisGroupInput, UnifiedHrisGroupOutput } from '../types/model.unified'; - +import { + UnifiedHrisGroupInput, + UnifiedHrisGroupOutput, +} from '../types/model.unified'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; -import { OriginalGroupOutput } from '@@core/utils/types/original/original.hris'; - -import { IGroupService } from '../types'; @Injectable() export class GroupService { @@ -26,14 +23,79 @@ export class GroupService { } async getGroup( - id_grouping_group: string, + id_hris_group: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const group = await this.prisma.hris_groups.findUnique({ + where: { id_hris_group: id_hris_group }, + }); + + if (!group) { + throw new Error(`Group with ID ${id_hris_group} not found.`); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: group.id_hris_group, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedGroup: UnifiedHrisGroupOutput = { + id: group.id_hris_group, + parent_group: group.parent_group, + name: group.name, + type: group.type, + field_mappings: field_mappings, + remote_id: group.remote_id, + remote_created_at: group.remote_created_at, + created_at: group.created_at, + modified_at: group.modified_at, + remote_was_deleted: group.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: group.id_hris_group, + }, + }); + unifiedGroup.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.group.pull', + method: 'GET', + url: '/hris/group', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedGroup; + } catch (error) { + throw error; + } } async getGroups( @@ -44,7 +106,90 @@ export class GroupService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedHrisGroupOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const groups = await this.prisma.hris_groups.findMany({ + take: limit + 1, + cursor: cursor ? { id_hris_group: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = groups.length > limit; + if (hasNextPage) groups.pop(); + + const unifiedGroups = await Promise.all( + groups.map(async (group) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: group.id_hris_group, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedGroup: UnifiedHrisGroupOutput = { + id: group.id_hris_group, + parent_group: group.parent_group, + name: group.name, + type: group.type, + field_mappings: field_mappings, + remote_id: group.remote_id, + remote_created_at: group.remote_created_at, + created_at: group.created_at, + modified_at: group.modified_at, + remote_was_deleted: group.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: group.id_hris_group, + }, + }); + unifiedGroup.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedGroup; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.group.pull', + method: 'GET', + url: '/hris/groups', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedGroups, + next_cursor: hasNextPage + ? groups[groups.length - 1].id_hris_group + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/hris/group/services/gusto/index.ts b/packages/api/src/hris/group/services/gusto/index.ts new file mode 100644 index 000000000..a2741d417 --- /dev/null +++ b/packages/api/src/hris/group/services/gusto/index.ts @@ -0,0 +1,72 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { HrisObject } from '@hris/@lib/@types'; +import { IGroupService } from '@hris/group/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { GustoGroupOutput } from './types'; + +@Injectable() +export class GustoService implements IGroupService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + HrisObject.group.toUpperCase() + ':' + GustoService.name, + ); + this.registry.registerService('gusto', this); + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId, id_company } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'gusto', + vertical: 'hris', + }, + }); + + const company = await this.prisma.hris_companies.findUnique({ + where: { + id_hris_company: id_company as string, + }, + select: { + remote_id: true, + }, + }); + + const resp = await axios.get( + `${connection.account_url}/v1/companies/${company.remote_id}/departments`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced gusto groups !`); + + return { + data: resp.data, + message: 'Gusto groups retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/hris/group/services/gusto/mappers.ts b/packages/api/src/hris/group/services/gusto/mappers.ts new file mode 100644 index 000000000..b41edfe47 --- /dev/null +++ b/packages/api/src/hris/group/services/gusto/mappers.ts @@ -0,0 +1,61 @@ +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { Injectable } from '@nestjs/common'; +import { GustoGroupOutput } from './types'; +import { + UnifiedHrisGroupInput, + UnifiedHrisGroupOutput, +} from '@hris/group/types/model.unified'; +import { IGroupMapper } from '@hris/group/types'; +import { Utils } from '@hris/@lib/@utils'; + +@Injectable() +export class GustoGroupMapper implements IGroupMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService('hris', 'group', 'gusto', this); + } + + async desunify( + source: UnifiedHrisGroupInput, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return; + } + + async unify( + source: GustoGroupOutput | GustoGroupOutput[], + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleGroupToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((group) => + this.mapSingleGroupToUnified(group, connectionId, customFieldMappings), + ), + ); + } + + private async mapSingleGroupToUnified( + group: GustoGroupOutput, + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return { + remote_id: group.uuid || null, + remote_data: group, + name: group.title, + }; + } +} diff --git a/packages/api/src/hris/group/services/gusto/types.ts b/packages/api/src/hris/group/services/gusto/types.ts new file mode 100644 index 000000000..b1b3bdaac --- /dev/null +++ b/packages/api/src/hris/group/services/gusto/types.ts @@ -0,0 +1,12 @@ +export type GustoGroupOutput = Partial<{ + uuid: string; + company_uuid: string; + title: string; + version: string; + employees: [ + { + uuid: string; + }, + ]; + contractors: any[]; +}>; diff --git a/packages/api/src/hris/group/sync/sync.processor.ts b/packages/api/src/hris/group/sync/sync.processor.ts new file mode 100644 index 000000000..aecfb6a70 --- /dev/null +++ b/packages/api/src/hris/group/sync/sync.processor.ts @@ -0,0 +1,19 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; +import { Queues } from '@@core/@core-services/queues/types'; + +@Processor(Queues.SYNC_JOBS_WORKER) +export class SyncProcessor { + constructor(private syncService: SyncService) {} + + @Process('hris-sync-groups') + async handleSyncGroups(job: Job) { + try { + console.log(`Processing queue -> hris-sync-groups ${job.id}`); + await this.syncService.kickstartSync(); + } catch (error) { + console.error('Error syncing hris groups', error); + } + } +} diff --git a/packages/api/src/hris/group/sync/sync.service.ts b/packages/api/src/hris/group/sync/sync.service.ts index d616ce3ce..94888caf3 100644 --- a/packages/api/src/hris/group/sync/sync.service.ts +++ b/packages/api/src/hris/group/sync/sync.service.ts @@ -2,7 +2,6 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; @@ -10,6 +9,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedHrisGroupOutput } from '../types/model.unified'; import { IGroupService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { HRIS_PROVIDERS } from '@panora/shared'; +import { hris_groups as HrisGroup } from '@prisma/client'; +import { OriginalGroupOutput } from '@@core/utils/types/original/original.hris'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -19,23 +24,146 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('hris', 'group', this); } - saveToDb( + + async onModuleInit() { + // Initialization logic if needed + } + + @Cron('0 */12 * * *') // every 12 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing groups...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of HRIS_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId, id_company } = param; + const service: IGroupService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedHrisGroupOutput, + OriginalGroupOutput, + IGroupService + >(integrationId, linkedUserId, 'hris', 'group', service, [ + { + param: id_company, + paramName: 'id_company', + shouldPassToService: true, + shouldPassToIngest: true, + }, + ]); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + groups: UnifiedHrisGroupOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const groupResults: HrisGroup[] = []; - async onModuleInit() { - // Initialization logic - } + for (let i = 0; i < groups.length; i++) { + const group = groups[i]; + const originId = group.remote_id; + + let existingGroup = await this.prisma.hris_groups.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const groupData = { + parent_group: group.parent_group, + name: group.name, + type: group.type, + remote_id: originId, + remote_created_at: group.remote_created_at + ? new Date(group.remote_created_at) + : new Date(), + modified_at: new Date(), + remote_was_deleted: group.remote_was_deleted || false, + }; - // Additional methods and logic + if (existingGroup) { + existingGroup = await this.prisma.hris_groups.update({ + where: { + id_hris_group: existingGroup.id_hris_group, + }, + data: groupData, + }); + } else { + existingGroup = await this.prisma.hris_groups.create({ + data: { + ...groupData, + id_hris_group: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + groupResults.push(existingGroup); + + // Process field mappings + await this.ingestService.processFieldMappings( + group.field_mappings, + existingGroup.id_hris_group, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingGroup.id_hris_group, + remote_data[i], + ); + } + + return groupResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/hris/group/types/index.ts b/packages/api/src/hris/group/types/index.ts index 2ed5a1257..b34ea68bd 100644 --- a/packages/api/src/hris/group/types/index.ts +++ b/packages/api/src/hris/group/types/index.ts @@ -2,17 +2,10 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; import { UnifiedHrisGroupInput, UnifiedHrisGroupOutput } from './model.unified'; import { OriginalGroupOutput } from '@@core/utils/types/original/original.hris'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IGroupService { - addGroup( - groupData: DesunifyReturnType, - linkedUserId: string, - ): Promise>; - - syncGroups( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IGroupMapper { diff --git a/packages/api/src/hris/group/types/model.unified.ts b/packages/api/src/hris/group/types/model.unified.ts index c9e196fc2..6ce238ae6 100644 --- a/packages/api/src/hris/group/types/model.unified.ts +++ b/packages/api/src/hris/group/types/model.unified.ts @@ -1,3 +1,135 @@ -export class UnifiedHrisGroupInput {} +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsDateString, + IsBoolean, +} from 'class-validator'; -export class UnifiedHrisGroupOutput extends UnifiedHrisGroupInput {} +export type Type = + | 'TEAM' + | 'DEPARTMENT' + | 'COST_CENTER' + | 'BUSINESS_UNIT' + | 'GROUP'; +export class UnifiedHrisGroupInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the parent group', + }) + @IsUUID() + @IsOptional() + parent_group?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Engineering Team', + nullable: true, + description: 'The name of the group', + }) + @IsString() + @IsOptional() + name?: string; + + @ApiPropertyOptional({ + type: String, + example: 'DEPARTMENT', + enum: ['TEAM', 'DEPARTMENT', 'COST_CENTER', 'BUSINESS_UNIT', 'GROUP'], + nullable: true, + description: 'The type of the group', + }) + @IsString() + @IsOptional() + type?: Type | string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedHrisGroupOutput extends UnifiedHrisGroupInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the group record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'group_1234', + nullable: true, + description: 'The remote ID of the group in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: 'The remote data of the group in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The date when the group was created in the 3rd party system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The created date of the group record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The last modified date of the group record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; + + @ApiPropertyOptional({ + type: Boolean, + example: false, + nullable: true, + description: 'Indicates if the group was deleted in the remote system', + }) + @IsBoolean() + @IsOptional() + remote_was_deleted?: boolean; +} diff --git a/packages/api/src/hris/hris.module.ts b/packages/api/src/hris/hris.module.ts index 46c9effdc..87f23517b 100644 --- a/packages/api/src/hris/hris.module.ts +++ b/packages/api/src/hris/hris.module.ts @@ -13,6 +13,8 @@ import { PayGroupModule } from './paygroup/paygroup.module'; import { PayrollRunModule } from './payrollrun/payrollrun.module'; import { TimeoffModule } from './timeoff/timeoff.module'; import { TimeoffBalanceModule } from './timeoffbalance/timeoffbalance.module'; +import { TimesheetentryModule } from './timesheetentry/timesheetentry.module'; +import { HrisUnificationService } from './@lib/@unification'; @Module({ exports: [ @@ -30,7 +32,9 @@ import { TimeoffBalanceModule } from './timeoffbalance/timeoffbalance.module'; PayrollRunModule, TimeoffModule, TimeoffBalanceModule, + TimesheetentryModule, ], + providers: [HrisUnificationService], imports: [ BankInfoModule, BenefitModule, @@ -46,6 +50,7 @@ import { TimeoffBalanceModule } from './timeoffbalance/timeoffbalance.module'; PayrollRunModule, TimeoffModule, TimeoffBalanceModule, + TimesheetentryModule, ], }) export class HrisModule {} diff --git a/packages/api/src/hris/location/location.controller.ts b/packages/api/src/hris/location/location.controller.ts index 218f0a4a7..009a75e0e 100644 --- a/packages/api/src/hris/location/location.controller.ts +++ b/packages/api/src/hris/location/location.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -33,7 +35,6 @@ import { ApiPaginatedResponse, } from '@@core/utils/dtos/openapi.respone.dto'; - @ApiTags('hris/locations') @Controller('hris/locations') export class LocationController { @@ -57,6 +58,7 @@ export class LocationController { }) @ApiPaginatedResponse(UnifiedHrisLocationOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getLocations( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/hris/location/location.module.ts b/packages/api/src/hris/location/location.module.ts index 8f4e42f0c..443570585 100644 --- a/packages/api/src/hris/location/location.module.ts +++ b/packages/api/src/hris/location/location.module.ts @@ -1,34 +1,27 @@ import { Module } from '@nestjs/common'; import { LocationController } from './location.controller'; -import { SyncService } from './sync/sync.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { LocationService } from './services/location.service'; import { ServiceRegistry } from './services/registry.service'; -import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; - -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { BullModule } from '@nestjs/bull'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { SyncService } from './sync/sync.service'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; - +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; - +import { Utils } from '@hris/@lib/@utils'; +import { GustoLocationMapper } from './services/gusto/mappers'; +import { GustoService } from './services/gusto'; @Module({ controllers: [LocationController], providers: [ LocationService, CoreUnification, - + Utils, SyncService, WebhookService, - ServiceRegistry, - IngestDataService, + GustoLocationMapper, /* PROVIDERS SERVICES */ + GustoService, ], exports: [SyncService], }) diff --git a/packages/api/src/hris/location/services/gusto/index.ts b/packages/api/src/hris/location/services/gusto/index.ts new file mode 100644 index 000000000..4787af093 --- /dev/null +++ b/packages/api/src/hris/location/services/gusto/index.ts @@ -0,0 +1,97 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { HrisObject } from '@hris/@lib/@types'; +import { ILocationService } from '@hris/location/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { GustoLocationOutput } from './types'; + +@Injectable() +export class GustoService implements ILocationService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + HrisObject.location.toUpperCase() + ':' + GustoService.name, + ); + this.registry.registerService('gusto', this); + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId, id_employee } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'gusto', + vertical: 'hris', + }, + }); + + const employee = await this.prisma.hris_employees.findUnique({ + where: { + id_hris_employee: id_employee as string, + }, + select: { + remote_id: true, + }, + }); + + const resp = await axios.get( + `${connection.account_url}/v1/employees/${employee.remote_id}/home_addresses`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + + const resp_ = await axios.get( + `${connection.account_url}/v1/employees/${employee.remote_id}/work_addresses`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced gusto locations !`); + const resp_home = Array.isArray(resp.data) + ? resp.data.map((add) => ({ + ...add, + type: 'HOME', + })) + : []; + + const resp_work = Array.isArray(resp_.data) + ? resp_.data.map((add) => ({ + ...add, + type: 'WORK', + })) + : []; + + return { + data: [...resp_home, ...resp_work], + message: 'Gusto locations retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/hris/location/services/gusto/mappers.ts b/packages/api/src/hris/location/services/gusto/mappers.ts new file mode 100644 index 000000000..b7486ba71 --- /dev/null +++ b/packages/api/src/hris/location/services/gusto/mappers.ts @@ -0,0 +1,71 @@ +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { Injectable } from '@nestjs/common'; +import { GustoLocationOutput } from './types'; +import { + UnifiedHrisLocationInput, + UnifiedHrisLocationOutput, +} from '@hris/location/types/model.unified'; +import { ILocationMapper } from '@hris/location/types'; +import { Utils } from '@hris/@lib/@utils'; + +@Injectable() +export class GustoLocationMapper implements ILocationMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService('hris', 'location', 'gusto', this); + } + + async desunify( + source: UnifiedHrisLocationInput, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return; + } + + async unify( + source: GustoLocationOutput | GustoLocationOutput[], + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleLocationToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((location) => + this.mapSingleLocationToUnified( + location, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleLocationToUnified( + location: GustoLocationOutput, + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return { + remote_id: location.uuid || null, + remote_data: location, + street_1: location.street_1, + street_2: location.street_2, + city: location.city, + state: location.state, + zip_code: location.zip, + country: location.country, + location_type: location.type, + }; + } +} diff --git a/packages/api/src/hris/location/services/gusto/types.ts b/packages/api/src/hris/location/services/gusto/types.ts new file mode 100644 index 000000000..f53987155 --- /dev/null +++ b/packages/api/src/hris/location/services/gusto/types.ts @@ -0,0 +1,11 @@ +export type GustoLocationOutput = Partial<{ + uuid: string; + street_1: string; + street_2: string; + city: string; + state: string; + zip: string; + country: string; + active: boolean; + type: 'WORK' | 'HOME'; +}>; diff --git a/packages/api/src/hris/location/services/location.service.ts b/packages/api/src/hris/location/services/location.service.ts index 457541b11..111eab050 100644 --- a/packages/api/src/hris/location/services/location.service.ts +++ b/packages/api/src/hris/location/services/location.service.ts @@ -2,19 +2,13 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { UnifiedHrisLocationInput, UnifiedHrisLocationOutput, } from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; -import { OriginalLocationOutput } from '@@core/utils/types/original/original.hris'; - -import { ILocationService } from '../types'; @Injectable() export class LocationService { @@ -29,14 +23,87 @@ export class LocationService { } async getLocation( - id_locationing_location: string, + id_hris_location: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const location = await this.prisma.hris_locations.findUnique({ + where: { id_hris_location: id_hris_location }, + }); + + if (!location) { + throw new Error(`Location with ID ${id_hris_location} not found.`); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: location.id_hris_location, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedLocation: UnifiedHrisLocationOutput = { + id: location.id_hris_location, + name: location.name, + phone_number: location.phone_number, + street_1: location.street_1, + street_2: location.street_2, + city: location.city, + state: location.state, + zip_code: location.zip_code, + country: location.country, + employee_id: location.id_hris_employee || null, + company_id: location.id_hris_company || null, + location_type: location.location_type, + field_mappings: field_mappings, + remote_id: location.remote_id, + remote_created_at: location.remote_created_at, + created_at: location.created_at, + modified_at: location.modified_at, + remote_was_deleted: location.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: location.id_hris_location, + }, + }); + unifiedLocation.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.location.pull', + method: 'GET', + url: '/hris/location', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedLocation; + } catch (error) { + throw error; + } } async getLocations( @@ -47,7 +114,98 @@ export class LocationService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedHrisLocationOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const locations = await this.prisma.hris_locations.findMany({ + take: limit + 1, + cursor: cursor ? { id_hris_location: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = locations.length > limit; + if (hasNextPage) locations.pop(); + + const unifiedLocations = await Promise.all( + locations.map(async (location) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: location.id_hris_location, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedLocation: UnifiedHrisLocationOutput = { + id: location.id_hris_location, + name: location.name, + phone_number: location.phone_number, + street_1: location.street_1, + street_2: location.street_2, + city: location.city, + employee_id: location.id_hris_employee || null, + company_id: location.id_hris_company || null, + state: location.state, + zip_code: location.zip_code, + country: location.country, + location_type: location.location_type, + field_mappings: field_mappings, + remote_id: location.remote_id, + remote_created_at: location.remote_created_at, + created_at: location.created_at, + modified_at: location.modified_at, + remote_was_deleted: location.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: location.id_hris_location, + }, + }); + unifiedLocation.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedLocation; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.location.pull', + method: 'GET', + url: '/hris/locations', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedLocations, + next_cursor: hasNextPage + ? locations[locations.length - 1].id_hris_location + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/hris/location/sync/sync.processor.ts b/packages/api/src/hris/location/sync/sync.processor.ts new file mode 100644 index 000000000..8352a99e4 --- /dev/null +++ b/packages/api/src/hris/location/sync/sync.processor.ts @@ -0,0 +1,19 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; +import { Queues } from '@@core/@core-services/queues/types'; + +@Processor(Queues.SYNC_JOBS_WORKER) +export class SyncProcessor { + constructor(private syncService: SyncService) {} + + @Process('hris-sync-locations') + async handleSyncLocations(job: Job) { + try { + console.log(`Processing queue -> hris-sync-locations ${job.id}`); + await this.syncService.kickstartSync(); + } catch (error) { + console.error('Error syncing hris locations', error); + } + } +} diff --git a/packages/api/src/hris/location/sync/sync.service.ts b/packages/api/src/hris/location/sync/sync.service.ts index 8ea2af121..f6ae377a1 100644 --- a/packages/api/src/hris/location/sync/sync.service.ts +++ b/packages/api/src/hris/location/sync/sync.service.ts @@ -2,7 +2,6 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; @@ -10,6 +9,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedHrisLocationOutput } from '../types/model.unified'; import { ILocationService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { HRIS_PROVIDERS } from '@panora/shared'; +import { hris_locations as HrisLocation } from '@prisma/client'; +import { OriginalLocationOutput } from '@@core/utils/types/original/original.hris'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -19,23 +24,155 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('hris', 'location', this); } - saveToDb( + + async onModuleInit() { + // Initialization logic if needed + } + + @Cron('0 */12 * * *') // every 12 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing locations...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of HRIS_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId, id_employee } = param; + const service: ILocationService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedHrisLocationOutput, + OriginalLocationOutput, + ILocationService + >(integrationId, linkedUserId, 'hris', 'location', service, [ + { + param: id_employee, + paramName: 'id_employee', + shouldPassToService: true, + shouldPassToIngest: true, + }, + ]); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + locations: UnifiedHrisLocationOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + id_employee?: string, + ): Promise { + try { + const locationResults: HrisLocation[] = []; - async onModuleInit() { - // Initialization logic - } + for (let i = 0; i < locations.length; i++) { + const location = locations[i]; + const originId = location.remote_id; + + let existingLocation = await this.prisma.hris_locations.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const locationData = { + name: location.name, + phone_number: location.phone_number, + street_1: location.street_1, + street_2: location.street_2, + id_hris_employee: id_employee || null, + id_hris_company: location.company_id || null, + city: location.city, + state: location.state, + zip_code: location.zip_code, + country: location.country, + location_type: location.location_type, + remote_id: originId, + remote_created_at: location.remote_created_at + ? new Date(location.remote_created_at) + : new Date(), + modified_at: new Date(), + remote_was_deleted: location.remote_was_deleted || false, + }; - // Additional methods and logic + if (existingLocation) { + existingLocation = await this.prisma.hris_locations.update({ + where: { + id_hris_location: existingLocation.id_hris_location, + }, + data: locationData, + }); + } else { + existingLocation = await this.prisma.hris_locations.create({ + data: { + ...locationData, + id_hris_location: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + locationResults.push(existingLocation); + + // Process field mappings + await this.ingestService.processFieldMappings( + location.field_mappings, + existingLocation.id_hris_location, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingLocation.id_hris_location, + remote_data[i], + ); + } + + return locationResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/hris/location/types/index.ts b/packages/api/src/hris/location/types/index.ts index 74daa5cc4..d045e44b6 100644 --- a/packages/api/src/hris/location/types/index.ts +++ b/packages/api/src/hris/location/types/index.ts @@ -1,18 +1,14 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { UnifiedHrisLocationInput, UnifiedHrisLocationOutput } from './model.unified'; +import { + UnifiedHrisLocationInput, + UnifiedHrisLocationOutput, +} from './model.unified'; import { OriginalLocationOutput } from '@@core/utils/types/original/original.hris'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface ILocationService { - addLocation( - locationData: DesunifyReturnType, - linkedUserId: string, - ): Promise>; - - syncLocations( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface ILocationMapper { diff --git a/packages/api/src/hris/location/types/model.unified.ts b/packages/api/src/hris/location/types/model.unified.ts index d966a0e3c..7c2fa11da 100644 --- a/packages/api/src/hris/location/types/model.unified.ts +++ b/packages/api/src/hris/location/types/model.unified.ts @@ -1,3 +1,215 @@ -export class UnifiedHrisLocationInput {} +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsDateString, + IsBoolean, + IsIn, +} from 'class-validator'; -export class UnifiedHrisLocationOutput extends UnifiedHrisLocationInput {} +export type LocationType = 'WORK' | 'HOME'; +export class UnifiedHrisLocationInput { + @ApiPropertyOptional({ + type: String, + example: 'Headquarters', + nullable: true, + description: 'The name of the location', + }) + @IsString() + @IsOptional() + name?: string; + + @ApiPropertyOptional({ + type: String, + example: '+1234567890', + nullable: true, + description: 'The phone number of the location', + }) + @IsString() + @IsOptional() + phone_number?: string; + + @ApiPropertyOptional({ + type: String, + example: '123 Main St', + nullable: true, + description: 'The first line of the street address', + }) + @IsString() + @IsOptional() + street_1?: string; + + @ApiPropertyOptional({ + type: String, + example: 'Suite 456', + nullable: true, + description: 'The second line of the street address', + }) + @IsString() + @IsOptional() + street_2?: string; + + @ApiPropertyOptional({ + type: String, + example: 'San Francisco', + nullable: true, + description: 'The city of the location', + }) + @IsString() + @IsOptional() + city?: string; + + @ApiPropertyOptional({ + type: String, + example: 'CA', + nullable: true, + description: 'The state or region of the location', + }) + @IsString() + @IsOptional() + state?: string; + + @ApiPropertyOptional({ + type: String, + example: '94105', + nullable: true, + description: 'The zip or postal code of the location', + }) + @IsString() + @IsOptional() + zip_code?: string; + + @ApiPropertyOptional({ + type: String, + example: 'USA', + nullable: true, + description: 'The country of the location', + }) + @IsString() + @IsOptional() + country?: string; + + @ApiPropertyOptional({ + type: String, + example: 'WORK', + enum: ['WORK', 'HOME'], + nullable: true, + description: 'The type of the location', + }) + @IsString() + @IsIn(['WORK', 'HOME']) + @IsOptional() + location_type?: LocationType | string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the company associated with the location', + }) + @IsUUID() + @IsOptional() + company_id?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the employee associated with the location', + }) + @IsUUID() + @IsOptional() + employee_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedHrisLocationOutput extends UnifiedHrisLocationInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the location record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'location_1234', + nullable: true, + description: + 'The remote ID of the location in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the location in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: + 'The date when the location was created in the 3rd party system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The created date of the location record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The last modified date of the location record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; + + @ApiPropertyOptional({ + type: Boolean, + example: false, + nullable: true, + description: 'Indicates if the location was deleted in the remote system', + }) + @IsBoolean() + @IsOptional() + remote_was_deleted?: boolean; +} diff --git a/packages/api/src/hris/paygroup/paygroup.controller.ts b/packages/api/src/hris/paygroup/paygroup.controller.ts index 8cf874362..81e7d69a5 100644 --- a/packages/api/src/hris/paygroup/paygroup.controller.ts +++ b/packages/api/src/hris/paygroup/paygroup.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -33,7 +35,6 @@ import { ApiPaginatedResponse, } from '@@core/utils/dtos/openapi.respone.dto'; - @ApiTags('hris/paygroups') @Controller('hris/paygroups') export class PayGroupController { @@ -57,6 +58,7 @@ export class PayGroupController { }) @ApiPaginatedResponse(UnifiedHrisPaygroupOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getPayGroups( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/hris/paygroup/paygroup.module.ts b/packages/api/src/hris/paygroup/paygroup.module.ts index c628cfd49..d13fe5594 100644 --- a/packages/api/src/hris/paygroup/paygroup.module.ts +++ b/packages/api/src/hris/paygroup/paygroup.module.ts @@ -1,33 +1,21 @@ import { Module } from '@nestjs/common'; import { PayGroupController } from './paygroup.controller'; -import { SyncService } from './sync/sync.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PayGroupService } from './services/paygroup.service'; import { ServiceRegistry } from './services/registry.service'; -import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; - -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { BullModule } from '@nestjs/bull'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { SyncService } from './sync/sync.service'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; - +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; - +import { Utils } from '@hris/@lib/@utils'; @Module({ controllers: [PayGroupController], providers: [ PayGroupService, - + Utils, CoreUnification, - SyncService, WebhookService, - ServiceRegistry, - IngestDataService, /* PROVIDERS SERVICES */ ], diff --git a/packages/api/src/hris/paygroup/services/paygroup.service.ts b/packages/api/src/hris/paygroup/services/paygroup.service.ts index ab4713949..b21213494 100644 --- a/packages/api/src/hris/paygroup/services/paygroup.service.ts +++ b/packages/api/src/hris/paygroup/services/paygroup.service.ts @@ -2,19 +2,13 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { UnifiedHrisPaygroupInput, UnifiedHrisPaygroupOutput, } from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; -import { OriginalPayGroupOutput } from '@@core/utils/types/original/original.hris'; - -import { IPayGroupService } from '../types'; @Injectable() export class PayGroupService { @@ -29,14 +23,77 @@ export class PayGroupService { } async getPayGroup( - id_paygrouping_paygroup: string, + id_hris_pay_group: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const paygroup = await this.prisma.hris_pay_groups.findUnique({ + where: { id_hris_pay_group: id_hris_pay_group }, + }); + + if (!paygroup) { + throw new Error(`PayGroup with ID ${id_hris_pay_group} not found.`); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: paygroup.id_hris_pay_group, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedPayGroup: UnifiedHrisPaygroupOutput = { + id: paygroup.id_hris_pay_group, + pay_group_name: paygroup.pay_group_name, + field_mappings: field_mappings, + remote_id: paygroup.remote_id, + remote_created_at: paygroup.remote_created_at, + created_at: paygroup.created_at, + modified_at: paygroup.modified_at, + remote_was_deleted: paygroup.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: paygroup.id_hris_pay_group, + }, + }); + unifiedPayGroup.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.paygroup.pull', + method: 'GET', + url: '/hris/paygroup', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedPayGroup; + } catch (error) { + throw error; + } } async getPayGroups( @@ -47,7 +104,88 @@ export class PayGroupService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedHrisPaygroupOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const paygroups = await this.prisma.hris_pay_groups.findMany({ + take: limit + 1, + cursor: cursor ? { id_hris_pay_group: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = paygroups.length > limit; + if (hasNextPage) paygroups.pop(); + + const unifiedPayGroups = await Promise.all( + paygroups.map(async (paygroup) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: paygroup.id_hris_pay_group, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedPayGroup: UnifiedHrisPaygroupOutput = { + id: paygroup.id_hris_pay_group, + pay_group_name: paygroup.pay_group_name, + field_mappings: field_mappings, + remote_id: paygroup.remote_id, + remote_created_at: paygroup.remote_created_at, + created_at: paygroup.created_at, + modified_at: paygroup.modified_at, + remote_was_deleted: paygroup.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: paygroup.id_hris_pay_group, + }, + }); + unifiedPayGroup.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedPayGroup; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.paygroup.pull', + method: 'GET', + url: '/hris/paygroups', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedPayGroups, + next_cursor: hasNextPage + ? paygroups[paygroups.length - 1].id_hris_pay_group + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/hris/paygroup/sync/sync.processor.ts b/packages/api/src/hris/paygroup/sync/sync.processor.ts new file mode 100644 index 000000000..cfd0640ba --- /dev/null +++ b/packages/api/src/hris/paygroup/sync/sync.processor.ts @@ -0,0 +1,19 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; +import { Queues } from '@@core/@core-services/queues/types'; + +@Processor(Queues.SYNC_JOBS_WORKER) +export class SyncProcessor { + constructor(private syncService: SyncService) {} + + @Process('hris-sync-paygroups') + async handleSyncPayGroups(job: Job) { + try { + console.log(`Processing queue -> hris-sync-paygroups ${job.id}`); + await this.syncService.kickstartSync(); + } catch (error) { + console.error('Error syncing hris pay groups', error); + } + } +} diff --git a/packages/api/src/hris/paygroup/sync/sync.service.ts b/packages/api/src/hris/paygroup/sync/sync.service.ts index 28e050121..169ce2556 100644 --- a/packages/api/src/hris/paygroup/sync/sync.service.ts +++ b/packages/api/src/hris/paygroup/sync/sync.service.ts @@ -2,7 +2,6 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; @@ -10,6 +9,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedHrisPaygroupOutput } from '../types/model.unified'; import { IPayGroupService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { HRIS_PROVIDERS } from '@panora/shared'; +import { hris_pay_groups as HrisPayGroup } from '@prisma/client'; +import { OriginalPayGroupOutput } from '@@core/utils/types/original/original.hris'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -19,23 +24,137 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('hris', 'paygroup', this); } - saveToDb( + + async onModuleInit() { + // Initialization logic if needed + } + + @Cron('0 */12 * * *') // every 12 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing paygroups...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of HRIS_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IPayGroupService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedHrisPaygroupOutput, + OriginalPayGroupOutput, + IPayGroupService + >(integrationId, linkedUserId, 'hris', 'paygroup', service, []); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + paygroups: UnifiedHrisPaygroupOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const paygroupResults: HrisPayGroup[] = []; - async onModuleInit() { - // Initialization logic - } + for (let i = 0; i < paygroups.length; i++) { + const paygroup = paygroups[i]; + const originId = paygroup.remote_id; + + let existingPayGroup = await this.prisma.hris_pay_groups.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const paygroupData = { + pay_group_name: paygroup.pay_group_name, + remote_id: originId, + remote_created_at: paygroup.remote_created_at + ? new Date(paygroup.remote_created_at) + : new Date(), + modified_at: new Date(), + remote_was_deleted: paygroup.remote_was_deleted || false, + }; - // Additional methods and logic + if (existingPayGroup) { + existingPayGroup = await this.prisma.hris_pay_groups.update({ + where: { + id_hris_pay_group: existingPayGroup.id_hris_pay_group, + }, + data: paygroupData, + }); + } else { + existingPayGroup = await this.prisma.hris_pay_groups.create({ + data: { + ...paygroupData, + id_hris_pay_group: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + paygroupResults.push(existingPayGroup); + + // Process field mappings + await this.ingestService.processFieldMappings( + paygroup.field_mappings, + existingPayGroup.id_hris_pay_group, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingPayGroup.id_hris_pay_group, + remote_data[i], + ); + } + + return paygroupResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/hris/paygroup/types/index.ts b/packages/api/src/hris/paygroup/types/index.ts index e3f361b31..d302e1edc 100644 --- a/packages/api/src/hris/paygroup/types/index.ts +++ b/packages/api/src/hris/paygroup/types/index.ts @@ -1,18 +1,14 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { UnifiedHrisPaygroupInput, UnifiedHrisPaygroupOutput } from './model.unified'; +import { + UnifiedHrisPaygroupInput, + UnifiedHrisPaygroupOutput, +} from './model.unified'; import { OriginalPayGroupOutput } from '@@core/utils/types/original/original.hris'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IPayGroupService { - addPayGroup( - paygroupData: DesunifyReturnType, - linkedUserId: string, - ): Promise>; - - syncPayGroups( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IPayGroupMapper { diff --git a/packages/api/src/hris/paygroup/types/model.unified.ts b/packages/api/src/hris/paygroup/types/model.unified.ts index d07174303..cb8c35487 100644 --- a/packages/api/src/hris/paygroup/types/model.unified.ts +++ b/packages/api/src/hris/paygroup/types/model.unified.ts @@ -1,3 +1,111 @@ -export class UnifiedHrisPaygroupInput {} +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsDateString, + IsBoolean, +} from 'class-validator'; -export class UnifiedHrisPaygroupOutput extends UnifiedHrisPaygroupInput {} +export class UnifiedHrisPaygroupInput { + @ApiPropertyOptional({ + type: String, + example: 'Monthly Salaried', + nullable: true, + description: 'The name of the pay group', + }) + @IsString() + @IsOptional() + pay_group_name?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedHrisPaygroupOutput extends UnifiedHrisPaygroupInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the pay group record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'paygroup_1234', + nullable: true, + description: + 'The remote ID of the pay group in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the pay group in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: + 'The date when the pay group was created in the 3rd party system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The created date of the pay group record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The last modified date of the pay group record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; + + @ApiPropertyOptional({ + type: Boolean, + example: false, + nullable: true, + description: 'Indicates if the pay group was deleted in the remote system', + }) + @IsBoolean() + @IsOptional() + remote_was_deleted?: boolean; +} diff --git a/packages/api/src/hris/payrollrun/payrollrun.controller.ts b/packages/api/src/hris/payrollrun/payrollrun.controller.ts index e9a84b27a..b0f31b6a7 100644 --- a/packages/api/src/hris/payrollrun/payrollrun.controller.ts +++ b/packages/api/src/hris/payrollrun/payrollrun.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -33,7 +35,6 @@ import { ApiPaginatedResponse, } from '@@core/utils/dtos/openapi.respone.dto'; - @ApiTags('hris/payrollruns') @Controller('hris/payrollruns') export class PayrollRunController { @@ -57,6 +58,7 @@ export class PayrollRunController { }) @ApiPaginatedResponse(UnifiedHrisPayrollrunOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getPayrollRuns( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/hris/payrollrun/payrollrun.module.ts b/packages/api/src/hris/payrollrun/payrollrun.module.ts index 2e94d346d..a7ffb3724 100644 --- a/packages/api/src/hris/payrollrun/payrollrun.module.ts +++ b/packages/api/src/hris/payrollrun/payrollrun.module.ts @@ -1,33 +1,21 @@ import { Module } from '@nestjs/common'; import { PayrollRunController } from './payrollrun.controller'; -import { SyncService } from './sync/sync.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PayrollRunService } from './services/payrollrun.service'; import { ServiceRegistry } from './services/registry.service'; -import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; - -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { BullModule } from '@nestjs/bull'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { SyncService } from './sync/sync.service'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; - +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; - +import { Utils } from '@hris/@lib/@utils'; @Module({ controllers: [PayrollRunController], providers: [ PayrollRunService, CoreUnification, - + Utils, SyncService, - WebhookService, - ServiceRegistry, - IngestDataService, /* PROVIDERS SERVICES */ ], diff --git a/packages/api/src/hris/payrollrun/services/payrollrun.service.ts b/packages/api/src/hris/payrollrun/services/payrollrun.service.ts index 7f0d145c0..3c66e6ff1 100644 --- a/packages/api/src/hris/payrollrun/services/payrollrun.service.ts +++ b/packages/api/src/hris/payrollrun/services/payrollrun.service.ts @@ -1,20 +1,11 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { - UnifiedHrisPayrollrunInput, - UnifiedHrisPayrollrunOutput, -} from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; +import { UnifiedHrisPayrollrunOutput } from '../types/model.unified'; import { ServiceRegistry } from './registry.service'; -import { OriginalPayrollRunOutput } from '@@core/utils/types/original/original.hris'; - -import { IPayrollRunService } from '../types'; @Injectable() export class PayrollRunService { @@ -29,14 +20,81 @@ export class PayrollRunService { } async getPayrollRun( - id_payrollruning_payrollrun: string, + id_hris_payroll_run: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const payrollRun = await this.prisma.hris_payroll_runs.findUnique({ + where: { id_hris_payroll_run: id_hris_payroll_run }, + }); + + if (!payrollRun) { + throw new Error(`PayrollRun with ID ${id_hris_payroll_run} not found.`); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: payrollRun.id_hris_payroll_run, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedPayrollRun: UnifiedHrisPayrollrunOutput = { + id: payrollRun.id_hris_payroll_run, + run_state: payrollRun.run_state, + run_type: payrollRun.run_type, + start_date: payrollRun.start_date, + end_date: payrollRun.end_date, + check_date: payrollRun.check_date, + field_mappings: field_mappings, + remote_id: payrollRun.remote_id, + remote_created_at: payrollRun.remote_created_at, + created_at: payrollRun.created_at, + modified_at: payrollRun.modified_at, + remote_was_deleted: payrollRun.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: payrollRun.id_hris_payroll_run, + }, + }); + unifiedPayrollRun.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.payrollrun.pull', + method: 'GET', + url: '/hris/payrollrun', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedPayrollRun; + } catch (error) { + throw error; + } } async getPayrollRuns( @@ -47,7 +105,92 @@ export class PayrollRunService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedHrisPayrollrunOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const payrollRuns = await this.prisma.hris_payroll_runs.findMany({ + take: limit + 1, + cursor: cursor ? { id_hris_payroll_run: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = payrollRuns.length > limit; + if (hasNextPage) payrollRuns.pop(); + + const unifiedPayrollRuns = await Promise.all( + payrollRuns.map(async (payrollRun) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: payrollRun.id_hris_payroll_run, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedPayrollRun: UnifiedHrisPayrollrunOutput = { + id: payrollRun.id_hris_payroll_run, + run_state: payrollRun.run_state, + run_type: payrollRun.run_type, + start_date: payrollRun.start_date, + end_date: payrollRun.end_date, + check_date: payrollRun.check_date, + field_mappings: field_mappings, + remote_id: payrollRun.remote_id, + remote_created_at: payrollRun.remote_created_at, + created_at: payrollRun.created_at, + modified_at: payrollRun.modified_at, + remote_was_deleted: payrollRun.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: payrollRun.id_hris_payroll_run, + }, + }); + unifiedPayrollRun.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedPayrollRun; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.payrollrun.pull', + method: 'GET', + url: '/hris/payrollruns', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedPayrollRuns, + next_cursor: hasNextPage + ? payrollRuns[payrollRuns.length - 1].id_hris_payroll_run + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/hris/payrollrun/sync/sync.processor.ts b/packages/api/src/hris/payrollrun/sync/sync.processor.ts new file mode 100644 index 000000000..088f45634 --- /dev/null +++ b/packages/api/src/hris/payrollrun/sync/sync.processor.ts @@ -0,0 +1,19 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; +import { Queues } from '@@core/@core-services/queues/types'; + +@Processor(Queues.SYNC_JOBS_WORKER) +export class SyncProcessor { + constructor(private syncService: SyncService) {} + + @Process('hris-sync-payrollruns') + async handleSyncCompanies(job: Job) { + try { + console.log(`Processing queue -> hris-sync-payrollruns ${job.id}`); + await this.syncService.kickstartSync(); + } catch (error) { + console.error('Error syncing hris payrollruns', error); + } + } +} diff --git a/packages/api/src/hris/payrollrun/sync/sync.service.ts b/packages/api/src/hris/payrollrun/sync/sync.service.ts index 943e06001..923773d7b 100644 --- a/packages/api/src/hris/payrollrun/sync/sync.service.ts +++ b/packages/api/src/hris/payrollrun/sync/sync.service.ts @@ -2,7 +2,6 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { Cron } from '@nestjs/schedule'; -import { ApiResponse } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; @@ -10,6 +9,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedHrisPayrollrunOutput } from '../types/model.unified'; import { IPayrollRunService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { HRIS_PROVIDERS } from '@panora/shared'; +import { hris_payroll_runs as HrisPayrollRun } from '@prisma/client'; +import { OriginalPayrollRunOutput } from '@@core/utils/types/original/original.hris'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -19,23 +24,145 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('hris', 'payrollrun', this); } - saveToDb( + + async onModuleInit() { + // Initialization logic if needed + } + + @Cron('0 */12 * * *') // every 12 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing payroll runs...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of HRIS_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: IPayrollRunService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedHrisPayrollrunOutput, + OriginalPayrollRunOutput, + IPayrollRunService + >(integrationId, linkedUserId, 'hris', 'payrollrun', service, []); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + payrollRuns: UnifiedHrisPayrollrunOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const payrollRunResults: HrisPayrollRun[] = []; - async onModuleInit() { - // Initialization logic - } + for (let i = 0; i < payrollRuns.length; i++) { + const payrollRun = payrollRuns[i]; + const originId = payrollRun.remote_id; + + let existingPayrollRun = await this.prisma.hris_payroll_runs.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const payrollRunData = { + run_state: payrollRun.run_state, + run_type: payrollRun.run_type, + start_date: payrollRun.start_date + ? new Date(payrollRun.start_date) + : null, + end_date: payrollRun.end_date ? new Date(payrollRun.end_date) : null, + check_date: payrollRun.check_date + ? new Date(payrollRun.check_date) + : null, + remote_id: originId, + remote_created_at: payrollRun.remote_created_at + ? new Date(payrollRun.remote_created_at) + : new Date(), + modified_at: new Date(), + remote_was_deleted: payrollRun.remote_was_deleted || false, + }; - // Additional methods and logic + if (existingPayrollRun) { + existingPayrollRun = await this.prisma.hris_payroll_runs.update({ + where: { + id_hris_payroll_run: existingPayrollRun.id_hris_payroll_run, + }, + data: payrollRunData, + }); + } else { + existingPayrollRun = await this.prisma.hris_payroll_runs.create({ + data: { + ...payrollRunData, + id_hris_payroll_run: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + payrollRunResults.push(existingPayrollRun); + + // Process field mappings + await this.ingestService.processFieldMappings( + payrollRun.field_mappings, + existingPayrollRun.id_hris_payroll_run, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingPayrollRun.id_hris_payroll_run, + remote_data[i], + ); + } + + return payrollRunResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/hris/payrollrun/types/index.ts b/packages/api/src/hris/payrollrun/types/index.ts index 9aebceeb2..a8f390412 100644 --- a/packages/api/src/hris/payrollrun/types/index.ts +++ b/packages/api/src/hris/payrollrun/types/index.ts @@ -5,17 +5,10 @@ import { } from './model.unified'; import { OriginalPayrollRunOutput } from '@@core/utils/types/original/original.hris'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface IPayrollRunService { - addPayrollRun( - payrollrunData: DesunifyReturnType, - linkedUserId: string, - ): Promise>; - - syncPayrollRuns( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface IPayrollRunMapper { diff --git a/packages/api/src/hris/payrollrun/types/model.unified.ts b/packages/api/src/hris/payrollrun/types/model.unified.ts index 131ae365c..29a95f9fd 100644 --- a/packages/api/src/hris/payrollrun/types/model.unified.ts +++ b/packages/api/src/hris/payrollrun/types/model.unified.ts @@ -1,3 +1,177 @@ -export class UnifiedHrisPayrollrunInput {} +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsDateString, + IsBoolean, +} from 'class-validator'; -export class UnifiedHrisPayrollrunOutput extends UnifiedHrisPayrollrunInput {} +export type RunState = 'PAID' | 'DRAFT' | 'APPROVED' | 'FAILED' | 'CLOSE'; +export type RunType = + | 'REGULAR' + | 'OFF_CYCLE' + | 'CORRECTION' + | 'TERMINATION' + | 'SIGN_ON_BONUS'; +export class UnifiedHrisPayrollrunInput { + @ApiPropertyOptional({ + type: String, + example: 'PAID', + enum: ['PAID', 'DRAFT', 'APPROVED', 'FAILED', 'CLOSE'], + nullable: true, + description: 'The state of the payroll run', + }) + @IsString() + @IsOptional() + run_state?: RunState | string; + + @ApiPropertyOptional({ + type: String, + example: 'REGULAR', + enum: [ + 'REGULAR', + 'OFF_CYCLE', + 'CORRECTION', + 'TERMINATION', + 'SIGN_ON_BONUS', + ], + nullable: true, + description: 'The type of the payroll run', + }) + @IsString() + @IsOptional() + run_type?: RunType | string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-01-01T00:00:00Z', + nullable: true, + description: 'The start date of the payroll run', + }) + @IsDateString() + @IsOptional() + start_date?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-01-15T23:59:59Z', + nullable: true, + description: 'The end date of the payroll run', + }) + @IsDateString() + @IsOptional() + end_date?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-01-20T00:00:00Z', + nullable: true, + description: 'The check date of the payroll run', + }) + @IsDateString() + @IsOptional() + check_date?: Date; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedHrisPayrollrunOutput extends UnifiedHrisPayrollrunInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the payroll run record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'payroll_run_1234', + nullable: true, + description: + 'The remote ID of the payroll run in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the payroll run in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: + 'The date when the payroll run was created in the 3rd party system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The created date of the payroll run record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: 'The last modified date of the payroll run record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; + + @ApiPropertyOptional({ + type: Boolean, + example: false, + nullable: true, + description: + 'Indicates if the payroll run was deleted in the remote system', + }) + @IsBoolean() + @IsOptional() + remote_was_deleted?: boolean; + + @ApiPropertyOptional({ + type: [String], + example: ['801f9ede-c698-4e66-a7fc-48d19eebaa4f'], + nullable: true, + description: + 'The UUIDs of the employee payroll runs associated with this payroll run', + }) + @IsOptional() + employee_payroll_runs?: string[]; +} diff --git a/packages/api/src/hris/timeoff/services/timeoff.service.ts b/packages/api/src/hris/timeoff/services/timeoff.service.ts index 843085e2f..780c1d37a 100644 --- a/packages/api/src/hris/timeoff/services/timeoff.service.ts +++ b/packages/api/src/hris/timeoff/services/timeoff.service.ts @@ -9,12 +9,13 @@ import { UnifiedHrisTimeoffInput, UnifiedHrisTimeoffOutput, } from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from './registry.service'; import { OriginalTimeoffOutput } from '@@core/utils/types/original/original.hris'; - +import { HrisObject } from '@panora/shared'; import { ITimeoffService } from '../types'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class TimeoffService { @@ -23,11 +24,21 @@ export class TimeoffService { private logger: LoggerService, private webhook: WebhookService, private fieldMappingService: FieldMappingService, + private coreUnification: CoreUnification, + private ingestService: IngestDataService, private serviceRegistry: ServiceRegistry, ) { this.logger.setContext(TimeoffService.name); } + async validateLinkedUser(linkedUserId: string) { + const linkedUser = await this.prisma.linked_users.findUnique({ + where: { id_linked_user: linkedUserId }, + }); + if (!linkedUser) throw new ReferenceError('Linked User Not Found'); + return linkedUser; + } + async addTimeoff( unifiedTimeoffData: UnifiedHrisTimeoffInput, connectionId: string, @@ -36,18 +47,210 @@ export class TimeoffService { linkedUserId: string, remote_data?: boolean, ): Promise { - return; + try { + const linkedUser = await this.validateLinkedUser(linkedUserId); + // Add any necessary validations here, e.g., validateEmployeeId if needed + + const desunifiedObject = + await this.coreUnification.desunify({ + sourceObject: unifiedTimeoffData, + targetType: HrisObject.timeoff, + providerName: integrationId, + vertical: 'hris', + customFieldMappings: [], + }); + + const service: ITimeoffService = + this.serviceRegistry.getService(integrationId); + const resp: ApiResponse = await service.addTimeoff( + desunifiedObject, + linkedUserId, + ); + + const unifiedObject = (await this.coreUnification.unify< + OriginalTimeoffOutput[] + >({ + sourceObject: [resp.data], + targetType: HrisObject.timeoff, + providerName: integrationId, + vertical: 'hris', + connectionId: connectionId, + customFieldMappings: [], + })) as UnifiedHrisTimeoffOutput[]; + + const source_timeoff = resp.data; + const target_timeoff = unifiedObject[0]; + + const unique_hris_timeoff_id = await this.saveOrUpdateTimeoff( + target_timeoff, + connectionId, + ); + + await this.ingestService.processRemoteData( + unique_hris_timeoff_id, + source_timeoff, + ); + + const result_timeoff = await this.getTimeoff( + unique_hris_timeoff_id, + undefined, + undefined, + connectionId, + projectId, + remote_data, + ); + + const status_resp = resp.statusCode === 201 ? 'success' : 'fail'; + const event = await this.prisma.events.create({ + data: { + id_connection: connectionId, + id_project: projectId, + id_event: uuidv4(), + status: status_resp, + type: 'hris.timeoff.push', + method: 'POST', + url: '/hris/timeoff', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + + await this.webhook.dispatchWebhook( + result_timeoff, + 'hris.timeoff.created', + linkedUser.id_project, + event.id_event, + ); + + return result_timeoff; + } catch (error) { + throw error; + } + } + + async saveOrUpdateTimeoff( + timeoff: UnifiedHrisTimeoffOutput, + connectionId: string, + ): Promise { + const existingTimeoff = await this.prisma.hris_time_off.findFirst({ + where: { remote_id: timeoff.remote_id, id_connection: connectionId }, + }); + + const data: any = { + employee: timeoff.employee, + approver: timeoff.approver, + status: timeoff.status, + employee_note: timeoff.employee_note, + units: timeoff.units, + amount: timeoff.amount ? BigInt(timeoff.amount) : null, + request_type: timeoff.request_type, + start_time: timeoff.start_time ? new Date(timeoff.start_time) : null, + end_time: timeoff.end_time ? new Date(timeoff.end_time) : null, + field_mappings: timeoff.field_mappings, + modified_at: new Date(), + }; + + if (existingTimeoff) { + const res = await this.prisma.hris_time_off.update({ + where: { id_hris_time_off: existingTimeoff.id_hris_time_off }, + data: data, + }); + + return res.id_hris_time_off; + } else { + data.created_at = new Date(); + data.remote_id = timeoff.remote_id; + data.id_connection = connectionId; + data.id_hris_time_off = uuidv4(); + data.remote_was_deleted = timeoff.remote_was_deleted ?? false; + data.remote_created_at = timeoff.remote_created_at + ? new Date(timeoff.remote_created_at) + : null; + + const newTimeoff = await this.prisma.hris_time_off.create({ data: data }); + + return newTimeoff.id_hris_time_off; + } } async getTimeoff( - id_timeoffing_timeoff: string, + id_hris_time_off: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const timeOff = await this.prisma.hris_time_off.findUnique({ + where: { id_hris_time_off: id_hris_time_off }, + }); + + if (!timeOff) { + throw new Error(`Time off with ID ${id_hris_time_off} not found.`); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: timeOff.id_hris_time_off }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedTimeOff: UnifiedHrisTimeoffOutput = { + id: timeOff.id_hris_time_off, + employee: timeOff.employee, + approver: timeOff.approver, + status: timeOff.status, + employee_note: timeOff.employee_note, + units: timeOff.units, + amount: timeOff.amount ? Number(timeOff.amount) : undefined, + request_type: timeOff.request_type, + start_time: timeOff.start_time, + end_time: timeOff.end_time, + field_mappings: field_mappings, + remote_id: timeOff.remote_id, + remote_created_at: timeOff.remote_created_at, + created_at: timeOff.created_at, + modified_at: timeOff.modified_at, + remote_was_deleted: timeOff.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: timeOff.id_hris_time_off }, + }); + unifiedTimeOff.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.time_off.pull', + method: 'GET', + url: '/hris/time_off', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedTimeOff; + } catch (error) { + throw error; + } } async getTimeoffs( @@ -58,7 +261,94 @@ export class TimeoffService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedHrisTimeoffOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const timeOffs = await this.prisma.hris_time_off.findMany({ + take: limit + 1, + cursor: cursor ? { id_hris_time_off: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = timeOffs.length > limit; + if (hasNextPage) timeOffs.pop(); + + const unifiedTimeOffs = await Promise.all( + timeOffs.map(async (timeOff) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { ressource_owner_id: timeOff.id_hris_time_off }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedTimeOff: UnifiedHrisTimeoffOutput = { + id: timeOff.id_hris_time_off, + employee: timeOff.employee, + approver: timeOff.approver, + status: timeOff.status, + employee_note: timeOff.employee_note, + units: timeOff.units, + amount: timeOff.amount ? Number(timeOff.amount) : undefined, + request_type: timeOff.request_type, + start_time: timeOff.start_time, + end_time: timeOff.end_time, + field_mappings: field_mappings, + remote_id: timeOff.remote_id, + remote_created_at: timeOff.remote_created_at, + created_at: timeOff.created_at, + modified_at: timeOff.modified_at, + remote_was_deleted: timeOff.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: timeOff.id_hris_time_off, + }, + }); + unifiedTimeOff.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedTimeOff; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.timeoff.pull', + method: 'GET', + url: '/hris/timeoffs', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedTimeOffs, + next_cursor: hasNextPage + ? timeOffs[timeOffs.length - 1].id_hris_time_off + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/hris/timeoff/sync/sync.processor.ts b/packages/api/src/hris/timeoff/sync/sync.processor.ts new file mode 100644 index 000000000..229d422a6 --- /dev/null +++ b/packages/api/src/hris/timeoff/sync/sync.processor.ts @@ -0,0 +1,19 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; +import { Queues } from '@@core/@core-services/queues/types'; + +@Processor(Queues.SYNC_JOBS_WORKER) +export class SyncProcessor { + constructor(private syncService: SyncService) {} + + @Process('hris-sync-timeoffs') + async handleSyncTimeOffs(job: Job) { + try { + console.log(`Processing queue -> hris-sync-timeoffs ${job.id}`); + await this.syncService.kickstartSync(); + } catch (error) { + console.error('Error syncing hris time offs', error); + } + } +} diff --git a/packages/api/src/hris/timeoff/sync/sync.service.ts b/packages/api/src/hris/timeoff/sync/sync.service.ts index 301c000b5..be3841b5a 100644 --- a/packages/api/src/hris/timeoff/sync/sync.service.ts +++ b/packages/api/src/hris/timeoff/sync/sync.service.ts @@ -10,6 +10,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedHrisTimeoffOutput } from '../types/model.unified'; import { ITimeoffService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { HRIS_PROVIDERS } from '@panora/shared'; +import { hris_time_off as HrisTimeOff } from '@prisma/client'; +import { OriginalTimeoffOutput } from '@@core/utils/types/original/original.hris'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -19,23 +25,143 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('hris', 'timeoff', this); } - saveToDb( + + async onModuleInit() { + // Initialization logic if needed + } + + @Cron('0 */12 * * *') // every 12 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing time off...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of HRIS_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: ITimeoffService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedHrisTimeoffOutput, + OriginalTimeoffOutput, + ITimeoffService + >(integrationId, linkedUserId, 'hris', 'timeoff', service, []); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + timeOffs: UnifiedHrisTimeoffOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const timeOffResults: HrisTimeOff[] = []; - async onModuleInit() { - // Initialization logic - } + for (let i = 0; i < timeOffs.length; i++) { + const timeOff = timeOffs[i]; + const originId = timeOff.remote_id; + + let existingTimeOff = await this.prisma.hris_time_off.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const timeOffData = { + employee: timeOff.employee, + approver: timeOff.approver, + status: timeOff.status, + employee_note: timeOff.employee_note, + units: timeOff.units, + amount: timeOff.amount ? BigInt(timeOff.amount) : null, + request_type: timeOff.request_type, + start_time: timeOff.start_time ? new Date(timeOff.start_time) : null, + end_time: timeOff.end_time ? new Date(timeOff.end_time) : null, + remote_id: originId, + remote_created_at: timeOff.remote_created_at + ? new Date(timeOff.remote_created_at) + : null, + modified_at: new Date(), + remote_was_deleted: timeOff.remote_was_deleted || false, + }; - // Additional methods and logic + if (existingTimeOff) { + existingTimeOff = await this.prisma.hris_time_off.update({ + where: { id_hris_time_off: existingTimeOff.id_hris_time_off }, + data: timeOffData, + }); + } else { + existingTimeOff = await this.prisma.hris_time_off.create({ + data: { + ...timeOffData, + id_hris_time_off: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + timeOffResults.push(existingTimeOff); + + // Process field mappings + await this.ingestService.processFieldMappings( + timeOff.field_mappings, + existingTimeOff.id_hris_time_off, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingTimeOff.id_hris_time_off, + remote_data[i], + ); + } + + return timeOffResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/hris/timeoff/timeoff.controller.ts b/packages/api/src/hris/timeoff/timeoff.controller.ts index 46099a778..d8895ecc1 100644 --- a/packages/api/src/hris/timeoff/timeoff.controller.ts +++ b/packages/api/src/hris/timeoff/timeoff.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -57,6 +59,7 @@ export class TimeoffController { }) @ApiPaginatedResponse(UnifiedHrisTimeoffOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getTimeoffs( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/hris/timeoff/timeoff.module.ts b/packages/api/src/hris/timeoff/timeoff.module.ts index f1b718b92..86d017aa9 100644 --- a/packages/api/src/hris/timeoff/timeoff.module.ts +++ b/packages/api/src/hris/timeoff/timeoff.module.ts @@ -1,32 +1,21 @@ import { Module } from '@nestjs/common'; -import { TimeoffController } from './timeoff.controller'; -import { SyncService } from './sync/sync.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { TimeoffService } from './services/timeoff.service'; import { ServiceRegistry } from './services/registry.service'; -import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; - -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { BullModule } from '@nestjs/bull'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { TimeoffService } from './services/timeoff.service'; +import { SyncService } from './sync/sync.service'; +import { TimeoffController } from './timeoff.controller'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; - +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; - +import { Utils } from '@hris/@lib/@utils'; @Module({ controllers: [TimeoffController], providers: [ TimeoffService, CoreUnification, - + Utils, SyncService, WebhookService, - ServiceRegistry, - IngestDataService, /* PROVIDERS SERVICES */ ], diff --git a/packages/api/src/hris/timeoff/types/index.ts b/packages/api/src/hris/timeoff/types/index.ts index 16e954446..e26f0d3ab 100644 --- a/packages/api/src/hris/timeoff/types/index.ts +++ b/packages/api/src/hris/timeoff/types/index.ts @@ -1,7 +1,11 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { UnifiedHrisTimeoffInput, UnifiedHrisTimeoffOutput } from './model.unified'; +import { + UnifiedHrisTimeoffInput, + UnifiedHrisTimeoffOutput, +} from './model.unified'; import { OriginalTimeoffOutput } from '@@core/utils/types/original/original.hris'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface ITimeoffService { addTimeoff( @@ -9,10 +13,7 @@ export interface ITimeoffService { linkedUserId: string, ): Promise>; - syncTimeoffs( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface ITimeoffMapper { diff --git a/packages/api/src/hris/timeoff/types/model.unified.ts b/packages/api/src/hris/timeoff/types/model.unified.ts index 09af4f654..ab9a25343 100644 --- a/packages/api/src/hris/timeoff/types/model.unified.ts +++ b/packages/api/src/hris/timeoff/types/model.unified.ts @@ -1,3 +1,216 @@ -export class UnifiedHrisTimeoffInput {} +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsNumber, + IsDateString, + IsBoolean, +} from 'class-validator'; -export class UnifiedHrisTimeoffOutput extends UnifiedHrisTimeoffInput {} +export type Status = + | 'REQUESTED' + | 'APPROVED' + | 'DECLINED' + | 'CANCELLED' + | 'DELETED'; + +export type RequestType = + | 'VACATION' + | 'SICK' + | 'PERSONAL' + | 'JURY_DUTY' + | 'VOLUNTEER' + | 'BEREAVEMENT'; +export class UnifiedHrisTimeoffInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the employee taking time off', + }) + @IsUUID() + @IsOptional() + employee?: string; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the approver for the time off request', + }) + @IsUUID() + @IsOptional() + approver?: string; + + @ApiPropertyOptional({ + type: String, + example: 'REQUESTED', + enum: ['REQUESTED', 'APPROVED', 'DECLINED', 'CANCELLED', 'DELETED'], + nullable: true, + description: 'The status of the time off request', + }) + @IsString() + @IsOptional() + status?: Status | string; + + @ApiPropertyOptional({ + type: String, + example: 'Annual vacation', + nullable: true, + description: 'A note from the employee about the time off request', + }) + @IsString() + @IsOptional() + employee_note?: string; + + @ApiPropertyOptional({ + type: String, + example: 'DAYS', + enum: ['HOURS', 'DAYS'], + nullable: true, + description: 'The units used for the time off (e.g., Days, Hours)', + }) + @IsString() + @IsOptional() + units?: 'HOURS' | 'DAYS' | string; + + @ApiPropertyOptional({ + type: Number, + example: 5, + nullable: true, + description: 'The amount of time off requested', + }) + @IsNumber() + @IsOptional() + amount?: number; + + @ApiPropertyOptional({ + type: String, + example: 'VACATION', + enum: [ + 'VACATION', + 'SICK', + 'PERSONAL', + 'JURY_DUTY', + 'VOLUNTEER', + 'BEREAVEMENT', + ], + nullable: true, + description: 'The type of time off request', + }) + @IsString() + @IsOptional() + request_type?: RequestType | string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-07-01T09:00:00Z', + nullable: true, + description: 'The start time of the time off', + }) + @IsDateString() + @IsOptional() + start_time?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-07-05T17:00:00Z', + nullable: true, + description: 'The end time of the time off', + }) + @IsDateString() + @IsOptional() + end_time?: Date; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedHrisTimeoffOutput extends UnifiedHrisTimeoffInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the time off record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'timeoff_1234', + nullable: true, + description: + 'The remote ID of the time off in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the time off in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: + 'The date when the time off was created in the 3rd party system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the time off record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the time off record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; + + @ApiPropertyOptional({ + type: Boolean, + example: false, + nullable: true, + description: 'Indicates if the time off was deleted in the remote system', + }) + @IsBoolean() + @IsOptional() + remote_was_deleted?: boolean; +} diff --git a/packages/api/src/hris/timeoffbalance/services/timeoffbalance.service.ts b/packages/api/src/hris/timeoffbalance/services/timeoffbalance.service.ts index 6a19702e2..33d58c218 100644 --- a/packages/api/src/hris/timeoffbalance/services/timeoffbalance.service.ts +++ b/packages/api/src/hris/timeoffbalance/services/timeoffbalance.service.ts @@ -1,20 +1,11 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { ApiResponse } from '@@core/utils/types'; -import { throwTypedError } from '@@core/utils/errors'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { - UnifiedHrisTimeoffbalanceInput, - UnifiedHrisTimeoffbalanceOutput, -} from '../types/model.unified'; - import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Injectable } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; +import { UnifiedHrisTimeoffbalanceOutput } from '../types/model.unified'; import { ServiceRegistry } from './registry.service'; -import { OriginalTimeoffBalanceOutput } from '@@core/utils/types/original/original.hris'; - -import { ITimeoffBalanceService } from '../types'; @Injectable() export class TimeoffBalanceService { @@ -29,14 +20,85 @@ export class TimeoffBalanceService { } async getTimeoffBalance( - id_timeoffbalanceing_timeoffbalance: string, + id_hris_time_off_balance: string, linkedUserId: string, integrationId: string, connectionId: string, projectId: string, remote_data?: boolean, ): Promise { - return; + try { + const timeOffBalance = + await this.prisma.hris_time_off_balances.findUnique({ + where: { id_hris_time_off_balance: id_hris_time_off_balance }, + }); + + if (!timeOffBalance) { + throw new Error( + `Time off balance with ID ${id_hris_time_off_balance} not found.`, + ); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: timeOffBalance.id_hris_time_off_balance, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedTimeOffBalance: UnifiedHrisTimeoffbalanceOutput = { + id: timeOffBalance.id_hris_time_off_balance, + balance: timeOffBalance.balance + ? Number(timeOffBalance.balance) + : undefined, + employee_id: timeOffBalance.id_hris_employee, + used: timeOffBalance.used ? Number(timeOffBalance.used) : undefined, + policy_type: timeOffBalance.policy_type, + field_mappings: field_mappings, + remote_id: timeOffBalance.remote_id, + remote_created_at: timeOffBalance.remote_created_at, + created_at: timeOffBalance.created_at, + modified_at: timeOffBalance.modified_at, + remote_was_deleted: timeOffBalance.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: timeOffBalance.id_hris_time_off_balance, + }, + }); + unifiedTimeOffBalance.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.time_off_balance.pull', + method: 'GET', + url: '/hris/time_off_balance', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedTimeOffBalance; + } catch (error) { + throw error; + } } async getTimeoffBalances( @@ -47,7 +109,95 @@ export class TimeoffBalanceService { limit: number, remote_data?: boolean, cursor?: string, - ): Promise { - return; + ): Promise<{ + data: UnifiedHrisTimeoffbalanceOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const timeOffBalances = await this.prisma.hris_time_off_balances.findMany( + { + take: limit + 1, + cursor: cursor ? { id_hris_time_off_balance: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }, + ); + + const hasNextPage = timeOffBalances.length > limit; + if (hasNextPage) timeOffBalances.pop(); + + const unifiedTimeOffBalances = await Promise.all( + timeOffBalances.map(async (timeOffBalance) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: timeOffBalance.id_hris_time_off_balance, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedTimeOffBalance: UnifiedHrisTimeoffbalanceOutput = { + id: timeOffBalance.id_hris_time_off_balance, + balance: timeOffBalance.balance + ? Number(timeOffBalance.balance) + : undefined, + employee_id: timeOffBalance.id_hris_employee, + used: timeOffBalance.used ? Number(timeOffBalance.used) : undefined, + policy_type: timeOffBalance.policy_type, + field_mappings: field_mappings, + remote_id: timeOffBalance.remote_id, + remote_created_at: timeOffBalance.remote_created_at, + created_at: timeOffBalance.created_at, + modified_at: timeOffBalance.modified_at, + remote_was_deleted: timeOffBalance.remote_was_deleted, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: timeOffBalance.id_hris_time_off_balance, + }, + }); + unifiedTimeOffBalance.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedTimeOffBalance; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.time_off_balance.pull', + method: 'GET', + url: '/hris/time_off_balances', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedTimeOffBalances, + next_cursor: hasNextPage + ? timeOffBalances[timeOffBalances.length - 1].id_hris_time_off_balance + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } } } diff --git a/packages/api/src/hris/timeoffbalance/sync/sync.processor.ts b/packages/api/src/hris/timeoffbalance/sync/sync.processor.ts new file mode 100644 index 000000000..5cf30623b --- /dev/null +++ b/packages/api/src/hris/timeoffbalance/sync/sync.processor.ts @@ -0,0 +1,19 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; +import { Queues } from '@@core/@core-services/queues/types'; + +@Processor(Queues.SYNC_JOBS_WORKER) +export class SyncProcessor { + constructor(private syncService: SyncService) {} + + @Process('hris-sync-timeoffbalances') + async handleSyncTimeOffBalances(job: Job) { + try { + console.log(`Processing queue -> hris-sync-timeoffbalances ${job.id}`); + await this.syncService.kickstartSync(); + } catch (error) { + console.error('Error syncing hris time off balances', error); + } + } +} diff --git a/packages/api/src/hris/timeoffbalance/sync/sync.service.ts b/packages/api/src/hris/timeoffbalance/sync/sync.service.ts index da8d4ebb6..165f5fc2a 100644 --- a/packages/api/src/hris/timeoffbalance/sync/sync.service.ts +++ b/packages/api/src/hris/timeoffbalance/sync/sync.service.ts @@ -1,7 +1,6 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; - import { Cron } from '@nestjs/schedule'; import { ApiResponse } from '@@core/utils/types'; import { v4 as uuidv4 } from 'uuid'; @@ -11,6 +10,12 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { UnifiedHrisTimeoffbalanceOutput } from '../types/model.unified'; import { ITimeoffBalanceService } from '../types'; import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { HRIS_PROVIDERS } from '@panora/shared'; +import { hris_time_off_balances as HrisTimeOffBalance } from '@prisma/client'; +import { OriginalTimeoffBalanceOutput } from '@@core/utils/types/original/original.hris'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -20,23 +25,146 @@ export class SyncService implements OnModuleInit, IBaseSync { private webhook: WebhookService, private fieldMappingService: FieldMappingService, private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); + this.registry.registerService('hris', 'timeoffbalance', this); + } + + async onModuleInit() { + // Initialization logic if needed } - saveToDb( + + @Cron('0 */12 * * *') // every 12 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing time off balances...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of HRIS_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: ITimeoffBalanceService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedHrisTimeoffbalanceOutput, + OriginalTimeoffBalanceOutput, + ITimeoffBalanceService + >(integrationId, linkedUserId, 'hris', 'timeoffbalance', service, []); + } catch (error) { + throw error; + } + } + + async saveToDb( connection_id: string, linkedUserId: string, - data: any[], + timeOffBalances: UnifiedHrisTimeoffbalanceOutput[], originSource: string, remote_data: Record[], - ...rest: any - ): Promise { - throw new Error('Method not implemented.'); - } + ): Promise { + try { + const timeOffBalanceResults: HrisTimeOffBalance[] = []; - async onModuleInit() { - // Initialization logic - } + for (let i = 0; i < timeOffBalances.length; i++) { + const timeOffBalance = timeOffBalances[i]; + const originId = timeOffBalance.remote_id; + + let existingTimeOffBalance = + await this.prisma.hris_time_off_balances.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const timeOffBalanceData = { + balance: timeOffBalance.balance + ? BigInt(timeOffBalance.balance) + : null, + id_hris_employee: timeOffBalance.employee_id, + used: timeOffBalance.used ? BigInt(timeOffBalance.used) : null, + policy_type: timeOffBalance.policy_type, + remote_id: originId, + remote_created_at: timeOffBalance.remote_created_at + ? new Date(timeOffBalance.remote_created_at) + : null, + modified_at: new Date(), + remote_was_deleted: timeOffBalance.remote_was_deleted || false, + }; - // Additional methods and logic + if (existingTimeOffBalance) { + existingTimeOffBalance = + await this.prisma.hris_time_off_balances.update({ + where: { + id_hris_time_off_balance: + existingTimeOffBalance.id_hris_time_off_balance, + }, + data: timeOffBalanceData, + }); + } else { + existingTimeOffBalance = + await this.prisma.hris_time_off_balances.create({ + data: { + ...timeOffBalanceData, + id_hris_time_off_balance: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + timeOffBalanceResults.push(existingTimeOffBalance); + + // Process field mappings + await this.ingestService.processFieldMappings( + timeOffBalance.field_mappings, + existingTimeOffBalance.id_hris_time_off_balance, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingTimeOffBalance.id_hris_time_off_balance, + remote_data[i], + ); + } + + return timeOffBalanceResults; + } catch (error) { + throw error; + } + } } diff --git a/packages/api/src/hris/timeoffbalance/timeoffbalance.controller.ts b/packages/api/src/hris/timeoffbalance/timeoffbalance.controller.ts index f01602fbd..3e99ab567 100644 --- a/packages/api/src/hris/timeoffbalance/timeoffbalance.controller.ts +++ b/packages/api/src/hris/timeoffbalance/timeoffbalance.controller.ts @@ -8,6 +8,8 @@ import { Param, Headers, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { @@ -33,7 +35,6 @@ import { ApiPaginatedResponse, } from '@@core/utils/dtos/openapi.respone.dto'; - @ApiTags('hris/timeoffbalances') @Controller('hris/timeoffbalances') export class TimeoffBalanceController { @@ -57,6 +58,7 @@ export class TimeoffBalanceController { }) @ApiPaginatedResponse(UnifiedHrisTimeoffbalanceOutput) @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @Get() async getTimeoffBalances( @Headers('x-connection-token') connection_token: string, diff --git a/packages/api/src/hris/timeoffbalance/timeoffbalance.module.ts b/packages/api/src/hris/timeoffbalance/timeoffbalance.module.ts index 8934473ee..106e1bdec 100644 --- a/packages/api/src/hris/timeoffbalance/timeoffbalance.module.ts +++ b/packages/api/src/hris/timeoffbalance/timeoffbalance.module.ts @@ -1,32 +1,21 @@ import { Module } from '@nestjs/common'; -import { TimeoffBalanceController } from './timeoffbalance.controller'; -import { SyncService } from './sync/sync.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { TimeoffBalanceService } from './services/timeoffbalance.service'; import { ServiceRegistry } from './services/registry.service'; -import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; - -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { BullModule } from '@nestjs/bull'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { TimeoffBalanceService } from './services/timeoffbalance.service'; +import { SyncService } from './sync/sync.service'; +import { TimeoffBalanceController } from './timeoffbalance.controller'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; - +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; - +import { Utils } from '@hris/@lib/@utils'; @Module({ controllers: [TimeoffBalanceController], providers: [ TimeoffBalanceService, CoreUnification, - + Utils, SyncService, WebhookService, - ServiceRegistry, - IngestDataService, /* PROVIDERS SERVICES */ ], diff --git a/packages/api/src/hris/timeoffbalance/types/index.ts b/packages/api/src/hris/timeoffbalance/types/index.ts index a8e19d9b4..aef0aaf01 100644 --- a/packages/api/src/hris/timeoffbalance/types/index.ts +++ b/packages/api/src/hris/timeoffbalance/types/index.ts @@ -5,17 +5,10 @@ import { } from './model.unified'; import { OriginalTimeoffBalanceOutput } from '@@core/utils/types/original/original.hris'; import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; export interface ITimeoffBalanceService { - addTimeoffBalance( - timeoffbalanceData: DesunifyReturnType, - linkedUserId: string, - ): Promise>; - - syncTimeoffBalances( - linkedUserId: string, - custom_properties?: string[], - ): Promise>; + sync(data: SyncParam): Promise>; } export interface ITimeoffBalanceMapper { @@ -34,5 +27,7 @@ export interface ITimeoffBalanceMapper { slug: string; remote_id: string; }[], - ): Promise; + ): Promise< + UnifiedHrisTimeoffbalanceOutput | UnifiedHrisTimeoffbalanceOutput[] + >; } diff --git a/packages/api/src/hris/timeoffbalance/types/model.unified.ts b/packages/api/src/hris/timeoffbalance/types/model.unified.ts index 47ec3b04f..ae45a58ae 100644 --- a/packages/api/src/hris/timeoffbalance/types/model.unified.ts +++ b/packages/api/src/hris/timeoffbalance/types/model.unified.ts @@ -1,3 +1,159 @@ -export class UnifiedHrisTimeoffbalanceInput {} +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsNumber, + IsDateString, + IsBoolean, +} from 'class-validator'; -export class UnifiedHrisTimeoffbalanceOutput extends UnifiedHrisTimeoffbalanceInput {} +export type PolicyType = + | 'VACATION' + | 'SICK' + | 'PERSONAL' + | 'JURY_DUTY' + | 'VOLUNTEER' + | 'BEREAVEMENT'; + +export class UnifiedHrisTimeoffbalanceInput { + @ApiPropertyOptional({ + type: Number, + example: 80, + nullable: true, + description: 'The current balance of time off', + }) + @IsNumber() + @IsOptional() + balance?: number; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated employee', + }) + @IsUUID() + @IsOptional() + employee_id?: string; + + @ApiPropertyOptional({ + type: Number, + example: 40, + nullable: true, + description: 'The amount of time off used', + }) + @IsNumber() + @IsOptional() + used?: number; + + @ApiPropertyOptional({ + type: String, + example: 'VACATION', + enum: [ + 'VACATION', + 'SICK', + 'PERSONAL', + 'JURY_DUTY', + 'VOLUNTEER', + 'BEREAVEMENT', + ], + nullable: true, + description: 'The type of time off policy', + }) + @IsString() + @IsOptional() + policy_type?: PolicyType | string; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedHrisTimeoffbalanceOutput extends UnifiedHrisTimeoffbalanceInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the time off balance record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'timeoff_balance_1234', + nullable: true, + description: + 'The remote ID of the time off balance in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the time off balance in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; + + @ApiPropertyOptional({ + type: String, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: + 'The date when the time off balance was created in the 3rd party system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: String, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The created date of the time off balance record', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: String, + example: '2024-06-15T12:00:00Z', + nullable: true, + description: 'The last modified date of the time off balance record', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; + + @ApiPropertyOptional({ + type: Boolean, + example: false, + nullable: true, + description: + 'Indicates if the time off balance was deleted in the remote system', + }) + @IsBoolean() + @IsOptional() + remote_was_deleted?: boolean; +} diff --git a/packages/api/src/hris/timesheetentry/services/registry.service.ts b/packages/api/src/hris/timesheetentry/services/registry.service.ts new file mode 100644 index 000000000..232f9b894 --- /dev/null +++ b/packages/api/src/hris/timesheetentry/services/registry.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { ITimesheetentryService } from '../types'; + +@Injectable() +export class ServiceRegistry { + private serviceMap: Map; + + constructor() { + this.serviceMap = new Map(); + } + + registerService(serviceKey: string, service: ITimesheetentryService) { + this.serviceMap.set(serviceKey, service); + } + + getService(integrationId: string): ITimesheetentryService { + const service = this.serviceMap.get(integrationId); + if (!service) { + throw new ReferenceError(); + } + return service; + } +} diff --git a/packages/api/src/hris/timesheetentry/services/timesheetentry.service.ts b/packages/api/src/hris/timesheetentry/services/timesheetentry.service.ts new file mode 100644 index 000000000..aa7661e54 --- /dev/null +++ b/packages/api/src/hris/timesheetentry/services/timesheetentry.service.ts @@ -0,0 +1,370 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { v4 as uuidv4 } from 'uuid'; +import { ApiResponse } from '@@core/utils/types'; +import { throwTypedError } from '@@core/utils/errors'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { + UnifiedHrisTimesheetEntryInput, + UnifiedHrisTimesheetEntryOutput, +} from '../types/model.unified'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { ServiceRegistry } from './registry.service'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { OriginalTimesheetentryOutput } from '@@core/utils/types/original/original.hris'; +import { HrisObject } from '@panora/shared'; +import { ITimesheetentryService } from '../types'; + +@Injectable() +export class TimesheetentryService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private webhook: WebhookService, + private fieldMappingService: FieldMappingService, + private coreUnification: CoreUnification, + private ingestService: IngestDataService, + private serviceRegistry: ServiceRegistry, + ) { + this.logger.setContext(TimesheetentryService.name); + } + + async validateLinkedUser(linkedUserId: string) { + const linkedUser = await this.prisma.linked_users.findUnique({ + where: { id_linked_user: linkedUserId }, + }); + if (!linkedUser) throw new ReferenceError('Linked User Not Found'); + return linkedUser; + } + + async addTimesheetentry( + unifiedTimesheetentryData: UnifiedHrisTimesheetEntryInput, + connectionId: string, + projectId: string, + integrationId: string, + linkedUserId: string, + remote_data?: boolean, + ): Promise { + try { + const linkedUser = await this.validateLinkedUser(linkedUserId); + // Add any necessary validations here, e.g., validateEmployeeId if needed + + const desunifiedObject = + await this.coreUnification.desunify({ + sourceObject: unifiedTimesheetentryData, + targetType: HrisObject.timesheetentry, + providerName: integrationId, + vertical: 'hris', + customFieldMappings: [], + }); + + const service: ITimesheetentryService = + this.serviceRegistry.getService(integrationId); + const resp: ApiResponse = + await service.addTimesheetentry(desunifiedObject, linkedUserId); + + const unifiedObject = (await this.coreUnification.unify< + OriginalTimesheetentryOutput[] + >({ + sourceObject: [resp.data], + targetType: HrisObject.timesheetentry, + providerName: integrationId, + vertical: 'hris', + connectionId: connectionId, + customFieldMappings: [], + })) as UnifiedHrisTimesheetEntryOutput[]; + + const source_timesheetentry = resp.data; + const target_timesheetentry = unifiedObject[0]; + + const unique_hris_timesheetentry_id = + await this.saveOrUpdateTimesheetentry( + target_timesheetentry, + connectionId, + ); + + await this.ingestService.processRemoteData( + unique_hris_timesheetentry_id, + source_timesheetentry, + ); + + const result_timesheetentry = await this.getTimesheetentry( + unique_hris_timesheetentry_id, + undefined, + undefined, + connectionId, + projectId, + remote_data, + ); + + const status_resp = resp.statusCode === 201 ? 'success' : 'fail'; + const event = await this.prisma.events.create({ + data: { + id_connection: connectionId, + id_project: projectId, + id_event: uuidv4(), + status: status_resp, + type: 'hris.timesheetentry.push', + method: 'POST', + url: '/hris/timesheetentries', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + + await this.webhook.dispatchWebhook( + result_timesheetentry, + 'hris.timesheetentry.created', + linkedUser.id_project, + event.id_event, + ); + + return result_timesheetentry; + } catch (error) { + throw error; + } + } + + private async saveOrUpdateTimesheetentry( + timesheetentry: UnifiedHrisTimesheetEntryOutput, + connectionId: string, + ): Promise { + const existingTimesheetentry = + await this.prisma.hris_timesheet_entries.findFirst({ + where: { + remote_id: timesheetentry.remote_id, + id_connection: connectionId, + }, + }); + + const data: any = { + hours_worked: timesheetentry.hours_worked + ? BigInt(timesheetentry.hours_worked) + : null, + start_time: timesheetentry.start_time + ? new Date(timesheetentry.start_time) + : null, + end_time: timesheetentry.end_time + ? new Date(timesheetentry.end_time) + : null, + id_hris_employee: timesheetentry.employee_id, + remote_was_deleted: timesheetentry.remote_was_deleted ?? false, + modified_at: new Date(), + }; + + // Only include field_mappings if it exists in the input + if (timesheetentry.field_mappings) { + data.field_mappings = timesheetentry.field_mappings; + } + + if (existingTimesheetentry) { + const res = await this.prisma.hris_timesheet_entries.update({ + where: { + id_hris_timesheet_entry: + existingTimesheetentry.id_hris_timesheet_entry, + }, + data: data, + }); + + return res.id_hris_timesheet_entry; + } else { + data.created_at = new Date(); + data.remote_id = timesheetentry.remote_id; + data.id_connection = connectionId; + data.id_hris_timesheet_entry = uuidv4(); + data.remote_created_at = timesheetentry.remote_created_at + ? new Date(timesheetentry.remote_created_at) + : null; + + const newTimesheetentry = await this.prisma.hris_timesheet_entries.create( + { data: data }, + ); + + return newTimesheetentry.id_hris_timesheet_entry; + } + } + + async getTimesheetentry( + id_hris_timesheet_entry: string, + linkedUserId: string, + integrationId: string, + connectionId: string, + projectId: string, + remote_data?: boolean, + ): Promise { + try { + const timesheetEntry = + await this.prisma.hris_timesheet_entries.findUnique({ + where: { id_hris_timesheet_entry: id_hris_timesheet_entry }, + }); + + if (!timesheetEntry) { + throw new Error( + `Timesheet entry with ID ${id_hris_timesheet_entry} not found.`, + ); + } + + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: timesheetEntry.id_hris_timesheet_entry, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedTimesheetEntry: UnifiedHrisTimesheetEntryOutput = { + id: timesheetEntry.id_hris_timesheet_entry, + hours_worked: timesheetEntry.hours_worked + ? Number(timesheetEntry.hours_worked) + : undefined, + start_time: timesheetEntry.start_time, + end_time: timesheetEntry.end_time, + employee_id: timesheetEntry.id_hris_employee, + remote_id: timesheetEntry.remote_id, + remote_created_at: timesheetEntry.remote_created_at, + created_at: timesheetEntry.created_at, + modified_at: timesheetEntry.modified_at, + remote_was_deleted: timesheetEntry.remote_was_deleted, + field_mappings: field_mappings, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { ressource_owner_id: timesheetEntry.id_hris_timesheet_entry }, + }); + unifiedTimesheetEntry.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.timesheetentry.pull', + method: 'GET', + url: '/hris/timesheetentry', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return unifiedTimesheetEntry; + } catch (error) { + throw error; + } + } + + async getTimesheetentrys( + connectionId: string, + projectId: string, + integrationId: string, + linkedUserId: string, + limit: number, + remote_data?: boolean, + cursor?: string, + ): Promise<{ + data: UnifiedHrisTimesheetEntryOutput[]; + next_cursor: string | null; + previous_cursor: string | null; + }> { + try { + const timesheetEntries = + await this.prisma.hris_timesheet_entries.findMany({ + take: limit + 1, + cursor: cursor ? { id_hris_timesheet_entry: cursor } : undefined, + where: { id_connection: connectionId }, + orderBy: { created_at: 'asc' }, + }); + + const hasNextPage = timesheetEntries.length > limit; + if (hasNextPage) timesheetEntries.pop(); + + const unifiedTimesheetEntries = await Promise.all( + timesheetEntries.map(async (timesheetEntry) => { + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: timesheetEntry.id_hris_timesheet_entry, + }, + }, + include: { attribute: true }, + }); + + const field_mappings = Object.fromEntries( + values.map((value) => [value.attribute.slug, value.data]), + ); + + const unifiedTimesheetEntry: UnifiedHrisTimesheetEntryOutput = { + id: timesheetEntry.id_hris_timesheet_entry, + hours_worked: timesheetEntry.hours_worked + ? Number(timesheetEntry.hours_worked) + : undefined, + start_time: timesheetEntry.start_time, + end_time: timesheetEntry.end_time, + employee_id: timesheetEntry.id_hris_employee, + remote_id: timesheetEntry.remote_id, + remote_created_at: timesheetEntry.remote_created_at, + created_at: timesheetEntry.created_at, + modified_at: timesheetEntry.modified_at, + remote_was_deleted: timesheetEntry.remote_was_deleted, + field_mappings: field_mappings, + }; + + if (remote_data) { + const remoteDataRecord = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: timesheetEntry.id_hris_timesheet_entry, + }, + }); + unifiedTimesheetEntry.remote_data = remoteDataRecord + ? JSON.parse(remoteDataRecord.data) + : null; + } + + return unifiedTimesheetEntry; + }), + ); + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'hris.timesheetentry.pull', + method: 'GET', + url: '/hris/timesheetentrys', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + id_project: projectId, + id_connection: connectionId, + }, + }); + + return { + data: unifiedTimesheetEntries, + next_cursor: hasNextPage + ? timesheetEntries[timesheetEntries.length - 1] + .id_hris_timesheet_entry + : null, + previous_cursor: cursor ?? null, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/hris/timesheetentry/sync/sync.processor.ts b/packages/api/src/hris/timesheetentry/sync/sync.processor.ts new file mode 100644 index 000000000..bb52e81c8 --- /dev/null +++ b/packages/api/src/hris/timesheetentry/sync/sync.processor.ts @@ -0,0 +1,19 @@ +import { Processor, Process } from '@nestjs/bull'; +import { Job } from 'bull'; +import { SyncService } from './sync.service'; +import { Queues } from '@@core/@core-services/queues/types'; + +@Processor(Queues.SYNC_JOBS_WORKER) +export class SyncProcessor { + constructor(private syncService: SyncService) {} + + @Process('hris-sync-timesheetentries') + async handleSyncTimesheetentries(job: Job) { + try { + console.log(`Processing queue -> hris-sync-timesheetentries ${job.id}`); + await this.syncService.kickstartSync(); + } catch (error) { + console.error('Error syncing hris timesheetentries', error); + } + } +} diff --git a/packages/api/src/hris/timesheetentry/sync/sync.service.ts b/packages/api/src/hris/timesheetentry/sync/sync.service.ts new file mode 100644 index 000000000..c35ad067c --- /dev/null +++ b/packages/api/src/hris/timesheetentry/sync/sync.service.ts @@ -0,0 +1,173 @@ +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { OriginalTimesheetentryOutput } from '@@core/utils/types/original/original.hris'; +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { Cron } from '@nestjs/schedule'; +import { HRIS_PROVIDERS } from '@panora/shared'; +import { hris_timesheet_entries as HrisTimesheetEntry } from '@prisma/client'; +import { v4 as uuidv4 } from 'uuid'; +import { ServiceRegistry } from '../services/registry.service'; +import { ITimesheetentryService } from '../types'; +import { UnifiedHrisTimesheetEntryOutput } from '../types/model.unified'; + +@Injectable() +export class SyncService implements OnModuleInit, IBaseSync { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private webhook: WebhookService, + private fieldMappingService: FieldMappingService, + private serviceRegistry: ServiceRegistry, + private coreUnification: CoreUnification, + private registry: CoreSyncRegistry, + private ingestService: IngestDataService, + ) { + this.logger.setContext(SyncService.name); + this.registry.registerService('hris', 'timesheetentry', this); + } + + async onModuleInit() { + // Initialization logic if needed + } + + @Cron('0 */8 * * *') // every 8 hours + async kickstartSync(user_id?: string) { + try { + this.logger.log('Syncing timesheet entries...'); + 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({ + where: { id_user: user.id_user }, + }); + for (const project of projects) { + const linkedUsers = await this.prisma.linked_users.findMany({ + where: { id_project: project.id_project }, + }); + for (const linkedUser of linkedUsers) { + for (const provider of HRIS_PROVIDERS) { + await this.syncForLinkedUser({ + integrationId: provider, + linkedUserId: linkedUser.id_linked_user, + }); + } + } + } + } + } + } catch (error) { + throw error; + } + } + + async syncForLinkedUser(param: SyncLinkedUserType) { + try { + const { integrationId, linkedUserId } = param; + const service: ITimesheetentryService = + this.serviceRegistry.getService(integrationId); + if (!service) return; + + await this.ingestService.syncForLinkedUser< + UnifiedHrisTimesheetEntryOutput, + OriginalTimesheetentryOutput, + ITimesheetentryService + >(integrationId, linkedUserId, 'hris', 'timesheetentry', service, []); + } catch (error) { + throw error; + } + } + + async saveToDb( + connection_id: string, + linkedUserId: string, + timesheetEntries: UnifiedHrisTimesheetEntryOutput[], + originSource: string, + remote_data: Record[], + ): Promise { + try { + const timesheetEntryResults: HrisTimesheetEntry[] = []; + + for (let i = 0; i < timesheetEntries.length; i++) { + const timesheetEntry = timesheetEntries[i]; + const originId = timesheetEntry.remote_id; + + let existingTimesheetEntry = + await this.prisma.hris_timesheet_entries.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); + + const timesheetEntryData = { + hours_worked: timesheetEntry.hours_worked + ? BigInt(timesheetEntry.hours_worked) + : null, + start_time: timesheetEntry.start_time + ? new Date(timesheetEntry.start_time) + : null, + end_time: timesheetEntry.end_time + ? new Date(timesheetEntry.end_time) + : null, + id_hris_employee: timesheetEntry.employee_id, + remote_id: originId, + remote_created_at: timesheetEntry.remote_created_at + ? new Date(timesheetEntry.remote_created_at) + : null, + modified_at: new Date(), + remote_was_deleted: timesheetEntry.remote_was_deleted || false, + }; + + if (existingTimesheetEntry) { + existingTimesheetEntry = + await this.prisma.hris_timesheet_entries.update({ + where: { + id_hris_timesheet_entry: + existingTimesheetEntry.id_hris_timesheet_entry, + }, + data: timesheetEntryData, + }); + } else { + existingTimesheetEntry = + await this.prisma.hris_timesheet_entries.create({ + data: { + ...timesheetEntryData, + id_hris_timesheet_entry: uuidv4(), + created_at: new Date(), + id_connection: connection_id, + }, + }); + } + + timesheetEntryResults.push(existingTimesheetEntry); + + // Process field mappings + await this.ingestService.processFieldMappings( + timesheetEntry.field_mappings, + existingTimesheetEntry.id_hris_timesheet_entry, + originSource, + linkedUserId, + ); + + // Process remote data + await this.ingestService.processRemoteData( + existingTimesheetEntry.id_hris_timesheet_entry, + remote_data[i], + ); + } + + return timesheetEntryResults; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/hris/timesheetentry/timesheetentry.controller.ts b/packages/api/src/hris/timesheetentry/timesheetentry.controller.ts new file mode 100644 index 000000000..7732a3d73 --- /dev/null +++ b/packages/api/src/hris/timesheetentry/timesheetentry.controller.ts @@ -0,0 +1,178 @@ +import { + Controller, + Post, + Body, + Query, + Get, + Patch, + Param, + Headers, + UseGuards, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { + ApiBody, + ApiOperation, + ApiParam, + ApiQuery, + ApiTags, + ApiHeader, + //ApiKeyAuth, +} from '@nestjs/swagger'; + +import { TimesheetentryService } from './services/timesheetentry.service'; +import { + UnifiedHrisTimesheetEntryInput, + UnifiedHrisTimesheetEntryOutput, +} from './types/model.unified'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { QueryDto } from '@@core/utils/dtos/query.dto'; +import { + ApiGetCustomResponse, + ApiPaginatedResponse, + ApiPostCustomResponse, +} from '@@core/utils/dtos/openapi.respone.dto'; + +@ApiTags('hris/timesheetentries') +@Controller('hris/timesheetentries') +export class TimesheetentryController { + constructor( + private readonly timesheetentryService: TimesheetentryService, + private logger: LoggerService, + private connectionUtils: ConnectionUtils, + ) { + this.logger.setContext(TimesheetentryController.name); + } + + @ApiOperation({ + operationId: 'listHrisTimesheetentries', + summary: 'List Timesheetentries', + }) + @ApiHeader({ + name: 'x-connection-token', + required: true, + description: 'The connection token', + example: 'b008e199-eda9-4629-bd41-a01b6195864a', + }) + @ApiPaginatedResponse(UnifiedHrisTimesheetEntryOutput) + @UseGuards(ApiKeyAuthGuard) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) + @Get() + async getTimesheetentrys( + @Headers('x-connection-token') connection_token: string, + @Query() query: QueryDto, + ) { + try { + const { linkedUserId, remoteSource, connectionId, projectId } = + await this.connectionUtils.getConnectionMetadataFromConnectionToken( + connection_token, + ); + const { remote_data, limit, cursor } = query; + return this.timesheetentryService.getTimesheetentrys( + connectionId, + projectId, + remoteSource, + linkedUserId, + limit, + remote_data, + cursor, + ); + } catch (error) { + throw new Error(error); + } + } + + @ApiOperation({ + operationId: 'retrieveHrisTimesheetentry', + summary: 'Retrieve Timesheetentry', + description: 'Retrieve an Timesheetentry from any connected Hris software', + }) + @ApiParam({ + name: 'id', + required: true, + type: String, + description: 'id of the timesheetentry you want to retrieve.', + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + }) + @ApiQuery({ + name: 'remote_data', + required: false, + type: Boolean, + description: 'Set to true to include data from the original Hris software.', + example: false, + }) + @ApiHeader({ + name: 'x-connection-token', + required: true, + description: 'The connection token', + example: 'b008e199-eda9-4629-bd41-a01b6195864a', + }) + @ApiGetCustomResponse(UnifiedHrisTimesheetEntryOutput) + @UseGuards(ApiKeyAuthGuard) + @Get(':id') + async retrieve( + @Headers('x-connection-token') connection_token: string, + @Param('id') id: string, + @Query('remote_data') remote_data?: boolean, + ) { + const { linkedUserId, remoteSource, connectionId, projectId } = + await this.connectionUtils.getConnectionMetadataFromConnectionToken( + connection_token, + ); + return this.timesheetentryService.getTimesheetentry( + id, + linkedUserId, + remoteSource, + connectionId, + projectId, + remote_data, + ); + } + + @ApiOperation({ + operationId: 'createHrisTimesheetentry', + summary: 'Create Timesheetentrys', + description: 'Create Timesheetentrys in any supported Hris software', + }) + @ApiHeader({ + name: 'x-connection-token', + required: true, + description: 'The connection token', + example: 'b008e199-eda9-4629-bd41-a01b6195864a', + }) + @ApiQuery({ + name: 'remote_data', + required: false, + type: Boolean, + description: 'Set to true to include data from the original Hris software.', + }) + @ApiBody({ type: UnifiedHrisTimesheetEntryInput }) + @ApiPostCustomResponse(UnifiedHrisTimesheetEntryOutput) + @UseGuards(ApiKeyAuthGuard) + @Post() + async addTimesheetentry( + @Body() unifiedTimesheetentryData: UnifiedHrisTimesheetEntryInput, + @Headers('x-connection-token') connection_token: string, + @Query('remote_data') remote_data?: boolean, + ) { + try { + const { linkedUserId, remoteSource, connectionId, projectId } = + await this.connectionUtils.getConnectionMetadataFromConnectionToken( + connection_token, + ); + return this.timesheetentryService.addTimesheetentry( + unifiedTimesheetentryData, + connectionId, + projectId, + remoteSource, + linkedUserId, + remote_data, + ); + } catch (error) { + throw new Error(error); + } + } +} diff --git a/packages/api/src/hris/timesheetentry/timesheetentry.module.ts b/packages/api/src/hris/timesheetentry/timesheetentry.module.ts new file mode 100644 index 000000000..5df86b0a2 --- /dev/null +++ b/packages/api/src/hris/timesheetentry/timesheetentry.module.ts @@ -0,0 +1,24 @@ +import { Module } from '@nestjs/common'; +import { TimesheetentryController } from './timesheetentry.controller'; +import { ServiceRegistry } from './services/registry.service'; +import { TimesheetentryService } from './services/timesheetentry.service'; +import { SyncService } from './sync/sync.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { Utils } from '@hris/@lib/@utils'; +@Module({ + controllers: [TimesheetentryController], + providers: [ + TimesheetentryService, + CoreUnification, + Utils, + SyncService, + WebhookService, + ServiceRegistry, + IngestDataService, + /* PROVIDERS SERVICES */ + ], + exports: [SyncService], +}) +export class TimesheetentryModule {} diff --git a/packages/api/src/hris/timesheetentry/types/index.ts b/packages/api/src/hris/timesheetentry/types/index.ts new file mode 100644 index 000000000..136dff390 --- /dev/null +++ b/packages/api/src/hris/timesheetentry/types/index.ts @@ -0,0 +1,38 @@ +import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; +import { + UnifiedHrisTimesheetEntryInput, + UnifiedHrisTimesheetEntryOutput, +} from './model.unified'; +import { OriginalTimesheetentryOutput } from '@@core/utils/types/original/original.hris'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; + +export interface ITimesheetentryService { + addTimesheetentry( + timesheetentryData: DesunifyReturnType, + linkedUserId: string, + ): Promise>; + + sync(data: SyncParam): Promise>; +} + +export interface ITimesheetentryMapper { + desunify( + source: UnifiedHrisTimesheetEntryInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): DesunifyReturnType; + + unify( + source: OriginalTimesheetentryOutput | OriginalTimesheetentryOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise< + UnifiedHrisTimesheetEntryOutput | UnifiedHrisTimesheetEntryOutput[] + >; +} diff --git a/packages/api/src/hris/timesheetentry/types/model.unified.ts b/packages/api/src/hris/timesheetentry/types/model.unified.ts new file mode 100644 index 000000000..5003c64f2 --- /dev/null +++ b/packages/api/src/hris/timesheetentry/types/model.unified.ts @@ -0,0 +1,137 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsUUID, + IsOptional, + IsString, + IsDateString, + IsBoolean, + IsNumber, +} from 'class-validator'; + +export class UnifiedHrisTimesheetEntryInput { + @ApiPropertyOptional({ + type: Number, + example: 40, + nullable: true, + description: 'The number of hours worked', + }) + @IsNumber() + @IsOptional() + hours_worked?: number; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T08:00:00Z', + nullable: true, + description: 'The start time of the timesheet entry', + }) + @IsOptional() + start_time?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T16:00:00Z', + nullable: true, + description: 'The end time of the timesheet entry', + }) + @IsOptional() + end_time?: Date; + + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the associated employee', + }) + @IsUUID() + @IsOptional() + employee_id?: string; + + @ApiPropertyOptional({ + type: Boolean, + example: false, + description: + 'Indicates if the timesheet entry was deleted in the remote system', + }) + @IsBoolean() + @IsOptional() + remote_was_deleted?: boolean; + + @ApiPropertyOptional({ + type: Object, + example: { + custom_field_1: 'value1', + custom_field_2: 'value2', + }, + nullable: true, + description: + 'The custom field mappings of the object between the remote 3rd party & Panora', + }) + @IsOptional() + field_mappings?: Record; +} + +export class UnifiedHrisTimesheetEntryOutput extends UnifiedHrisTimesheetEntryInput { + @ApiPropertyOptional({ + type: String, + example: '801f9ede-c698-4e66-a7fc-48d19eebaa4f', + nullable: true, + description: 'The UUID of the timesheet entry record', + }) + @IsUUID() + @IsOptional() + id?: string; + + @ApiPropertyOptional({ + type: String, + example: 'id_1', + nullable: true, + description: 'The remote ID of the timesheet entry', + }) + @IsString() + @IsOptional() + remote_id?: string; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + nullable: true, + description: + 'The date when the timesheet entry was created in the remote system', + }) + @IsDateString() + @IsOptional() + remote_created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + description: 'The created date of the timesheet entry', + }) + @IsDateString() + @IsOptional() + created_at?: Date; + + @ApiPropertyOptional({ + type: Date, + example: '2024-10-01T12:00:00Z', + description: 'The last modified date of the timesheet entry', + }) + @IsDateString() + @IsOptional() + modified_at?: Date; + + @ApiPropertyOptional({ + type: Object, + example: { + raw_data: { + additional_field: 'some value', + }, + }, + nullable: true, + description: + 'The remote data of the timesheet entry in the context of the 3rd Party', + }) + @IsOptional() + remote_data?: Record; +} diff --git a/packages/api/src/hris/timesheetentry/utils/index.ts b/packages/api/src/hris/timesheetentry/utils/index.ts new file mode 100644 index 000000000..f849788c1 --- /dev/null +++ b/packages/api/src/hris/timesheetentry/utils/index.ts @@ -0,0 +1 @@ +/* PUT ALL UTILS FUNCTIONS USED IN YOUR OBJECT METHODS HERE */ diff --git a/packages/api/src/main.ts b/packages/api/src/main.ts index 5412ba5de..456f471da 100644 --- a/packages/api/src/main.ts +++ b/packages/api/src/main.ts @@ -8,6 +8,7 @@ import * as fs from 'fs'; import * as yaml from 'js-yaml'; import { Logger, LoggerErrorInterceptor } from 'nestjs-pino'; import { AppModule } from './app.module'; +import { generatePanoraParamsSpec } from '@@core/utils/decorators/utils'; function addSpeakeasyGroup(document: any) { for (const path in document.paths) { @@ -87,8 +88,11 @@ async function bootstrap() { ], }; document['x-speakeasy-name-override'] = - extendedSpecs['x-speakeasy-name-override']; // Add extended specs + extendedSpecs['x-speakeasy-name-override']; addSpeakeasyGroup(document); + + await generatePanoraParamsSpec(document); + useContainer(app.select(AppModule), { fallbackOnErrors: true }); SwaggerModule.setup('docs', app, document); diff --git a/packages/api/src/ticketing/comment/services/github/mappers.ts b/packages/api/src/ticketing/comment/services/github/mappers.ts index 683eb4d01..b7b0f3fff 100644 --- a/packages/api/src/ticketing/comment/services/github/mappers.ts +++ b/packages/api/src/ticketing/comment/services/github/mappers.ts @@ -7,99 +7,98 @@ import { Utils } from '@ticketing/@lib/@utils'; import { UnifiedTicketingAttachmentOutput } from '@ticketing/attachment/types/model.unified'; import { ICommentMapper } from '@ticketing/comment/types'; import { - UnifiedTicketingCommentInput, - UnifiedTicketingCommentOutput, + UnifiedTicketingCommentInput, + UnifiedTicketingCommentOutput, } from '@ticketing/comment/types/model.unified'; import { GithubCommentInput, GithubCommentOutput } from './types'; @Injectable() export class GithubCommentMapper implements ICommentMapper { - constructor( - private mappersRegistry: MappersRegistry, - private utils: Utils, - private coreUnificationService: CoreUnification, - ) { - this.mappersRegistry.registerService( - 'ticketing', - 'comment', - 'github', - this, - ); - } + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService( + 'ticketing', + 'comment', + 'github', + this, + ); + } - async desunify( - source: UnifiedTicketingCommentInput, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): Promise { - // project_id and issue_id will be extracted and used so We do not need to set user (author) field here + async desunify( + source: UnifiedTicketingCommentInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + // project_id and issue_id will be extracted and used so We do not need to set user (author) field here - // TODO - Add attachments attribute + // TODO - Add attachments attribute - const result: GithubCommentInput = { - body: source.body, - }; - return result; - } + const result: GithubCommentInput = { + body: source.body, + }; + return result; + } - async unify( - source: GithubCommentOutput | GithubCommentOutput[], - connectionId: string, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): Promise { - if (!Array.isArray(source)) { - return await this.mapSingleCommentToUnified( - source, - connectionId, - customFieldMappings, - ); - } - return Promise.all( - source.map((comment) => - this.mapSingleCommentToUnified( - comment, - connectionId, - customFieldMappings, - ), - ), - ); + async unify( + source: GithubCommentOutput | GithubCommentOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return await this.mapSingleCommentToUnified( + source, + connectionId, + customFieldMappings, + ); } + return Promise.all( + source.map((comment) => + this.mapSingleCommentToUnified( + comment, + connectionId, + customFieldMappings, + ), + ), + ); + } - private async mapSingleCommentToUnified( - comment: GithubCommentOutput, - connectionId: string, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): Promise { - let opts: any = {}; - - // Here Github represent Attachment as URL in body of comment as Markdown so we do not have to store in attachement unified object. + private async mapSingleCommentToUnified( + comment: GithubCommentOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + let opts: any = {}; + // Here Github represent Attachment as URL in body of comment as Markdown so we do not have to store in attachement unified object. - if (comment.user.id) { - const user_id = await this.utils.getUserUuidFromRemoteId( - String(comment.user.id), - connectionId, - ); - if (user_id) { - opts = { ...opts, user_id }; - } - } - // GithubCommentOutput does not contain id of issue that it is assciated - - return { - remote_id: String(comment.id), - remote_data: comment, - body: comment.body || null, - creator_type: 'USER', - ...opts, - }; + if (comment.user.id) { + const user_id = await this.utils.getUserUuidFromRemoteId( + String(comment.user.id), + connectionId, + ); + if (user_id) { + opts = { ...opts, user_id }; + } } + // GithubCommentOutput does not contain id of issue that it is assciated + + return { + remote_id: String(comment.id), + remote_data: comment, + body: comment.body || null, + creator_type: 'USER', + ...opts, + }; + } } diff --git a/packages/api/swagger/openapi-with-code-samples.yaml b/packages/api/swagger/openapi-with-code-samples.yaml index 5c5d61f1a..63f5d5778 100644 --- a/packages/api/swagger/openapi-with-code-samples.yaml +++ b/packages/api/swagger/openapi-with-code-samples.yaml @@ -167,7 +167,7 @@ paths: x-codeSamples: - lang: typescript label: signIn - source: "import { Panora } from \"@panora/sdk\";\n\nconst panora = new Panora({\n apiKey: \"\",\n});\n\nasync function run() {\n await panora.auth.login.signIn({\n idUser: \"\",\n email: \"Oda.Treutel97@hotmail.com\",\n passwordHash: \"\",\n });\n\n \n}\n\nrun();" + source: "import { Panora } from \"@panora/sdk\";\n\nconst panora = new Panora({\n apiKey: \"\",\n});\n\nasync function run() {\n await panora.auth.login.signIn({\n email: \"Oda.Treutel97@hotmail.com\",\n passwordHash: \"\",\n });\n\n \n}\n\nrun();" - lang: python label: signIn source: |- @@ -483,6 +483,8 @@ paths: schema: type: string responses: + '200': + description: '' '201': description: '' content: @@ -566,6 +568,8 @@ paths: schema: type: string responses: + '200': + description: '' '201': description: '' content: @@ -659,6 +663,8 @@ paths: type: object additionalProperties: true description: Dynamic event payload + '201': + description: '' tags: *ref_0 x-speakeasy-group: webhooks x-codeSamples: @@ -765,6 +771,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -1216,6 +1223,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -1425,6 +1433,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -1629,6 +1638,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -1987,6 +1997,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -2400,6 +2411,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -2810,6 +2822,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -3151,6 +3164,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -3515,6 +3529,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -3857,6 +3872,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -4066,6 +4082,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -4415,6 +4432,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -4624,6 +4642,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -4834,6 +4853,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -5180,6 +5200,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -5389,6 +5410,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -6655,11 +6677,15 @@ paths: required: false in: query schema: + minimum: 1 + default: 1 type: number - name: limit required: false in: query schema: + minimum: 1 + default: 10 type: number responses: '200': @@ -6756,6 +6782,12 @@ paths: application/json: schema: type: object + '201': + description: '' + content: + application/json: + schema: + type: object tags: &ref_21 - passthrough x-speakeasy-group: passthrough @@ -6776,13 +6808,6 @@ paths: passThroughRequestDto: { method: PassThroughRequestDtoMethod.Get, path: "/dev", - data: {}, - requestFormat: { - "key": "", - }, - overrideBaseUrl: { - "key": "", - }, }, }); @@ -6930,6 +6955,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -7139,6 +7165,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -7348,6 +7375,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -7557,6 +7585,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -7766,6 +7795,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -7976,6 +8006,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -8284,6 +8315,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -8494,6 +8526,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -8703,6 +8736,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -8912,6 +8946,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -9121,6 +9156,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -9330,6 +9366,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -9539,6 +9576,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -9847,6 +9885,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -10056,6 +10095,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -10371,6 +10411,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -10688,6 +10729,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -11003,6 +11045,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -11318,6 +11361,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -11529,6 +11573,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -11740,6 +11785,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -12051,6 +12097,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -12262,6 +12309,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -12573,6 +12621,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -12784,6 +12833,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -13133,6 +13183,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -13498,6 +13549,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -13847,6 +13899,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -14300,6 +14353,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -14509,6 +14563,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -14876,6 +14931,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -15086,6 +15142,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -15295,6 +15352,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -15504,6 +15562,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -15713,6 +15772,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -15922,6 +15982,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -16131,6 +16192,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -16340,6 +16402,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -16549,6 +16612,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -16753,6 +16817,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -17065,6 +17130,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -17274,6 +17340,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -17587,6 +17654,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -17797,6 +17865,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -18007,6 +18076,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -18217,6 +18287,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -18529,6 +18600,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -18739,6 +18811,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -19051,6 +19124,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -19261,6 +19335,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -19573,6 +19648,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -19782,6 +19858,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -20095,6 +20172,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -20407,6 +20485,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -20617,6 +20696,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -20930,6 +21010,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -21139,6 +21220,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -21349,6 +21431,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -21559,6 +21642,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -21769,6 +21853,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -21978,6 +22063,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -22326,6 +22412,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -22677,6 +22764,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -22886,6 +22974,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -23095,6 +23184,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -23419,6 +23509,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -23538,7 +23629,12 @@ paths: label: createEcommerceOrder source: |- import { Panora } from "@panora/sdk"; - import { UnifiedEcommerceOrderInputCurrency } from "@panora/sdk/models/components"; + import { + UnifiedEcommerceOrderInputCurrency, + UnifiedEcommerceOrderInputFulfillmentStatus, + UnifiedEcommerceOrderInputOrderStatus, + UnifiedEcommerceOrderInputPaymentStatus, + } from "@panora/sdk/models/components"; const panora = new Panora({ apiKey: "", @@ -23549,17 +23645,19 @@ paths: xConnectionToken: "", remoteData: false, unifiedEcommerceOrderInput: { - orderStatus: "PAID", + orderStatus: UnifiedEcommerceOrderInputOrderStatus.Unshipped, orderNumber: "19823838833", - paymentStatus: "SUCCESS", + paymentStatus: UnifiedEcommerceOrderInputPaymentStatus.Success, currency: UnifiedEcommerceOrderInputCurrency.Aud, totalPrice: 300, totalDiscount: 10, totalShipping: 120, totalTax: 120, - fulfillmentStatus: "delivered", + fulfillmentStatus: UnifiedEcommerceOrderInputFulfillmentStatus.Pending, customerId: "801f9ede-c698-4e66-a7fc-48d19eebaa4f", - items: {}, + items: [ + {}, + ], fieldMappings: {}, }, }); @@ -23740,6 +23838,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -23944,6 +24043,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -24149,6 +24249,7 @@ paths: example: 10 description: Set to get the number of records. schema: + default: 50 type: number - name: cursor required: false @@ -24494,7 +24595,6 @@ components: password_hash: type: string required: - - id_user - email - password_hash Connection: @@ -24582,8 +24682,8 @@ components: description: The unique UUID of the webhook. endpoint_description: type: string - example: Webhook to receive connection events nullable: true + example: Webhook to receive connection events description: The description of the webhook. url: type: string @@ -24621,8 +24721,8 @@ components: last_update: format: date-time type: string - example: '2024-10-01T12:00:00Z' nullable: true + example: '2024-10-01T12:00:00Z' description: The last update date of the webhook. required: - id_webhook_endpoint @@ -24657,7 +24757,6 @@ components: type: string required: - url - - description - scope SignatureVerificationDto: type: object @@ -26748,8 +26847,6 @@ components: - id_project - name - sync_mode - - pull_frequency - - redirect_url - id_user - id_connector_set CreateProjectDto: @@ -27118,10 +27215,10 @@ components: type: object properties: method: - type: string enum: - GET - POST + type: string path: type: string nullable: true @@ -27140,12 +27237,11 @@ components: type: object additionalProperties: true nullable: true + headers: + type: object required: - method - path - - data - - request_format - - overrideBaseUrl UnifiedHrisBankinfoOutput: type: object properties: {} @@ -29529,12 +29625,20 @@ components: nullable: true description: >- The custom field mappings of the object between the remote 3rd party & Panora + LineItem: + type: object + properties: {} UnifiedEcommerceOrderOutput: type: object properties: order_status: type: string - example: PAID + example: UNSHIPPED + enum: &ref_150 + - PENDING + - UNSHIPPED + - SHIPPED + - CANCELED nullable: true description: The status of the order order_number: @@ -29545,13 +29649,16 @@ components: payment_status: type: string example: SUCCESS + enum: &ref_151 + - SUCCESS + - FAIL nullable: true description: The payment status of the order currency: type: string nullable: true example: AUD - enum: &ref_150 + enum: &ref_152 - AED - AFN - ALL @@ -29739,7 +29846,11 @@ components: fulfillment_status: type: string nullable: true - example: delivered + example: PENDING + enum: &ref_153 + - PENDING + - FULFILLED + - CANCELED description: The fulfillment status of the order customer_id: type: string @@ -29747,13 +29858,39 @@ components: nullable: true description: The UUID of the customer associated with the order items: - type: object nullable: true - example: &ref_151 {} + example: &ref_154 + - remote_id: '12345' + product_id: prod_001 + variant_id: var_001 + sku: SKU123 + title: Sample Product + quantity: 2 + price: '19.99' + total: '39.98' + fulfillment_status: PENDING + requires_shipping: true + taxable: true + weight: 1.5 + variant_title: Size M + vendor: Sample Vendor + properties: + - name: Color + value: Red + tax_lines: + - title: Sales Tax + price: '3.00' + rate: 0.075 + discount_allocations: + - amount: '5.00' + discount_application_index: 0 description: The items in the order + type: array + items: + $ref: '#/components/schemas/LineItem' field_mappings: type: object - example: &ref_152 + example: &ref_155 fav_dish: broccoli fav_color: red nullable: true @@ -29791,7 +29928,8 @@ components: properties: order_status: type: string - example: PAID + example: UNSHIPPED + enum: *ref_150 nullable: true description: The status of the order order_number: @@ -29802,13 +29940,14 @@ components: payment_status: type: string example: SUCCESS + enum: *ref_151 nullable: true description: The payment status of the order currency: type: string nullable: true example: AUD - enum: *ref_150 + enum: *ref_152 description: >- The currency of the order. Authorized value must be of type CurrencyCode (ISO 4217) total_price: @@ -29834,7 +29973,8 @@ components: fulfillment_status: type: string nullable: true - example: delivered + example: PENDING + enum: *ref_153 description: The fulfillment status of the order customer_id: type: string @@ -29842,13 +29982,15 @@ components: nullable: true description: The UUID of the customer associated with the order items: - type: object nullable: true - example: *ref_151 + example: *ref_154 description: The items in the order + type: array + items: + $ref: '#/components/schemas/LineItem' field_mappings: type: object - example: *ref_152 + example: *ref_155 nullable: true description: >- The custom field mappings of the object between the remote 3rd party & Panora @@ -30022,7 +30164,7 @@ components: field_mappings: type: object nullable: true - example: &ref_153 + example: &ref_156 fav_dish: broccoli fav_color: red description: >- @@ -30093,7 +30235,7 @@ components: field_mappings: type: object nullable: true - example: *ref_153 + example: *ref_156 description: >- The custom field mappings of the attachment between the remote 3rd party & Panora additionalProperties: true diff --git a/packages/api/swagger/swagger-spec.yaml b/packages/api/swagger/swagger-spec.yaml index e9cfb326b..166080fe7 100644 --- a/packages/api/swagger/swagger-spec.yaml +++ b/packages/api/swagger/swagger-spec.yaml @@ -107,8 +107,6 @@ paths: schema: type: string responses: - '200': - description: '' '201': description: '' content: @@ -129,8 +127,6 @@ paths: schema: type: string responses: - '200': - description: '' '201': description: '' content: @@ -161,8 +157,6 @@ paths: type: object additionalProperties: true description: Dynamic event payload - '201': - description: '' tags: *ref_0 x-speakeasy-group: webhooks /ticketing/tickets: @@ -2221,12 +2215,6 @@ paths: application/json: schema: type: object - '201': - description: '' - content: - application/json: - schema: - type: object tags: &ref_21 - passthrough x-speakeasy-group: passthrough @@ -3602,6 +3590,130 @@ paths: $ref: '#/components/schemas/UnifiedHrisTimeoffbalanceOutput' tags: *ref_35 x-speakeasy-group: hris.timeoffbalances + /hris/timesheetentries: + get: + operationId: listHrisTimesheetentries + summary: List Timesheetentries + parameters: + - name: x-connection-token + required: true + in: header + description: The connection token + schema: + type: string + - name: remote_data + required: false + in: query + example: true + description: Set to true to include data from the original software. + schema: + type: boolean + - name: limit + required: false + in: query + example: 10 + description: Set to get the number of records. + schema: + default: 50 + type: number + - name: cursor + required: false + in: query + example: 1b8b05bb-5273-4012-b520-8657b0b90874 + description: Set to get the number of records after this cursor. + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedDto' + - properties: + data: + type: array + items: + $ref: '#/components/schemas/UnifiedHrisTimesheetEntryOutput' + tags: &ref_36 + - hris/timesheetentries + x-speakeasy-group: hris.timesheetentries + x-speakeasy-pagination: + type: cursor + inputs: + - name: cursor + in: parameters + type: cursor + outputs: + nextCursor: $.next_cursor + post: + operationId: createHrisTimesheetentry + summary: Create Timesheetentrys + description: Create Timesheetentrys in any supported Hris 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 Hris software. + schema: + type: boolean + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UnifiedHrisTimesheetEntryInput' + responses: + '201': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/UnifiedHrisTimesheetEntryOutput' + tags: *ref_36 + x-speakeasy-group: hris.timesheetentries + /hris/timesheetentries/{id}: + get: + operationId: retrieveHrisTimesheetentry + summary: Retrieve Timesheetentry + description: Retrieve an Timesheetentry from any connected Hris software + parameters: + - name: x-connection-token + required: true + in: header + description: The connection token + schema: + type: string + - name: id + required: true + in: path + description: id of the timesheetentry you want to retrieve. + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + schema: + type: string + - name: remote_data + required: false + in: query + description: Set to true to include data from the original Hris software. + example: false + schema: + type: boolean + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/UnifiedHrisTimesheetEntryOutput' + tags: *ref_36 + x-speakeasy-group: hris.timesheetentries /marketingautomation/actions: get: operationId: listMarketingautomationAction @@ -3649,7 +3761,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedMarketingautomationActionOutput - tags: &ref_36 + tags: &ref_37 - marketingautomation/actions x-speakeasy-group: marketingautomation.actions x-speakeasy-pagination: @@ -3693,7 +3805,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedMarketingautomationActionOutput' - tags: *ref_36 + tags: *ref_37 x-speakeasy-group: marketingautomation.actions /marketingautomation/actions/{id}: get: @@ -3730,7 +3842,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedMarketingautomationActionOutput' - tags: *ref_36 + tags: *ref_37 x-speakeasy-group: marketingautomation.actions /marketingautomation/automations: get: @@ -3779,7 +3891,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedMarketingautomationAutomationOutput - tags: &ref_37 + tags: &ref_38 - marketingautomation/automations x-speakeasy-group: marketingautomation.automations x-speakeasy-pagination: @@ -3824,7 +3936,7 @@ paths: schema: $ref: >- #/components/schemas/UnifiedMarketingautomationAutomationOutput - tags: *ref_37 + tags: *ref_38 x-speakeasy-group: marketingautomation.automations /marketingautomation/automations/{id}: get: @@ -3862,7 +3974,7 @@ paths: schema: $ref: >- #/components/schemas/UnifiedMarketingautomationAutomationOutput - tags: *ref_37 + tags: *ref_38 x-speakeasy-group: marketingautomation.automations /marketingautomation/campaigns: get: @@ -3911,7 +4023,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedMarketingautomationCampaignOutput - tags: &ref_38 + tags: &ref_39 - marketingautomation/campaigns x-speakeasy-group: marketingautomation.campaigns x-speakeasy-pagination: @@ -3955,7 +4067,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedMarketingautomationCampaignOutput' - tags: *ref_38 + tags: *ref_39 x-speakeasy-group: marketingautomation.campaigns /marketingautomation/campaigns/{id}: get: @@ -3992,7 +4104,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedMarketingautomationCampaignOutput' - tags: *ref_38 + tags: *ref_39 x-speakeasy-group: marketingautomation.campaigns /marketingautomation/contacts: get: @@ -4041,7 +4153,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedMarketingautomationContactOutput - tags: &ref_39 + tags: &ref_40 - marketingautomation/contacts x-speakeasy-group: marketingautomation.contacts x-speakeasy-pagination: @@ -4085,7 +4197,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedMarketingautomationContactOutput' - tags: *ref_39 + tags: *ref_40 x-speakeasy-group: marketingautomation.contacts /marketingautomation/contacts/{id}: get: @@ -4122,7 +4234,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedMarketingautomationContactOutput' - tags: *ref_39 + tags: *ref_40 x-speakeasy-group: marketingautomation.contacts /marketingautomation/emails: get: @@ -4171,7 +4283,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedMarketingautomationEmailOutput - tags: &ref_40 + tags: &ref_41 - marketingautomation/emails x-speakeasy-group: marketingautomation.emails x-speakeasy-pagination: @@ -4217,7 +4329,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedMarketingautomationEmailOutput' - tags: *ref_40 + tags: *ref_41 x-speakeasy-group: marketingautomation.emails /marketingautomation/events: get: @@ -4266,7 +4378,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedMarketingautomationEventOutput - tags: &ref_41 + tags: &ref_42 - marketingautomation/events x-speakeasy-group: marketingautomation.events x-speakeasy-pagination: @@ -4312,7 +4424,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedMarketingautomationEventOutput' - tags: *ref_41 + tags: *ref_42 x-speakeasy-group: marketingautomation.events /marketingautomation/lists: get: @@ -4361,7 +4473,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedMarketingautomationListOutput - tags: &ref_42 + tags: &ref_43 - marketingautomation/lists x-speakeasy-group: marketingautomation.lists x-speakeasy-pagination: @@ -4404,7 +4516,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedMarketingautomationListOutput' - tags: *ref_42 + tags: *ref_43 x-speakeasy-group: marketingautomation.lists /marketingautomation/lists/{id}: get: @@ -4441,7 +4553,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedMarketingautomationListOutput' - tags: *ref_42 + tags: *ref_43 x-speakeasy-group: marketingautomation.lists /marketingautomation/messages: get: @@ -4490,7 +4602,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedMarketingautomationMessageOutput - tags: &ref_43 + tags: &ref_44 - marketingautomation/messages x-speakeasy-group: marketingautomation.messages x-speakeasy-pagination: @@ -4536,7 +4648,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedMarketingautomationMessageOutput' - tags: *ref_43 + tags: *ref_44 x-speakeasy-group: marketingautomation.messages /marketingautomation/templates: get: @@ -4585,7 +4697,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedMarketingautomationTemplateOutput - tags: &ref_44 + tags: &ref_45 - marketingautomation/templates x-speakeasy-group: marketingautomation.templates x-speakeasy-pagination: @@ -4628,7 +4740,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedMarketingautomationTemplateOutput' - tags: *ref_44 + tags: *ref_45 x-speakeasy-group: marketingautomation.templates /marketingautomation/templates/{id}: get: @@ -4665,7 +4777,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedMarketingautomationTemplateOutput' - tags: *ref_44 + tags: *ref_45 x-speakeasy-group: marketingautomation.templates /marketingautomation/users: get: @@ -4714,7 +4826,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedMarketingautomationUserOutput - tags: &ref_45 + tags: &ref_46 - marketingautomation/users x-speakeasy-group: marketingautomation.users x-speakeasy-pagination: @@ -4760,7 +4872,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedMarketingautomationUserOutput' - tags: *ref_45 + tags: *ref_46 x-speakeasy-group: marketingautomation.users /ats/activities: get: @@ -4808,7 +4920,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAtsActivityOutput' - tags: &ref_46 + tags: &ref_47 - ats/activities x-speakeasy-group: ats.activities x-speakeasy-pagination: @@ -4850,7 +4962,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsActivityOutput' - tags: *ref_46 + tags: *ref_47 x-speakeasy-group: ats.activities /ats/activities/{id}: get: @@ -4885,7 +4997,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsActivityOutput' - tags: *ref_46 + tags: *ref_47 x-speakeasy-group: ats.activities /ats/applications: get: @@ -4933,7 +5045,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAtsApplicationOutput' - tags: &ref_47 + tags: &ref_48 - ats/applications x-speakeasy-group: ats.applications x-speakeasy-pagination: @@ -4975,7 +5087,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsApplicationOutput' - tags: *ref_47 + tags: *ref_48 x-speakeasy-group: ats.applications /ats/applications/{id}: get: @@ -5010,7 +5122,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsApplicationOutput' - tags: *ref_47 + tags: *ref_48 x-speakeasy-group: ats.applications /ats/attachments: get: @@ -5058,7 +5170,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAtsAttachmentOutput' - tags: &ref_48 + tags: &ref_49 - ats/attachments x-speakeasy-group: ats.attachments x-speakeasy-pagination: @@ -5100,7 +5212,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsAttachmentOutput' - tags: *ref_48 + tags: *ref_49 x-speakeasy-group: ats.attachments /ats/attachments/{id}: get: @@ -5135,7 +5247,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsAttachmentOutput' - tags: *ref_48 + tags: *ref_49 x-speakeasy-group: ats.attachments /ats/candidates: get: @@ -5183,7 +5295,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAtsCandidateOutput' - tags: &ref_49 + tags: &ref_50 - ats/candidates x-speakeasy-group: ats.candidates x-speakeasy-pagination: @@ -5225,7 +5337,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsCandidateOutput' - tags: *ref_49 + tags: *ref_50 x-speakeasy-group: ats.candidates /ats/candidates/{id}: get: @@ -5260,7 +5372,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsCandidateOutput' - tags: *ref_49 + tags: *ref_50 x-speakeasy-group: ats.candidates /ats/departments: get: @@ -5308,7 +5420,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAtsDepartmentOutput' - tags: &ref_50 + tags: &ref_51 - ats/departments x-speakeasy-group: ats.departments x-speakeasy-pagination: @@ -5352,7 +5464,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsDepartmentOutput' - tags: *ref_50 + tags: *ref_51 x-speakeasy-group: ats.departments /ats/interviews: get: @@ -5400,7 +5512,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAtsInterviewOutput' - tags: &ref_51 + tags: &ref_52 - ats/interviews x-speakeasy-group: ats.interviews x-speakeasy-pagination: @@ -5442,7 +5554,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsInterviewOutput' - tags: *ref_51 + tags: *ref_52 x-speakeasy-group: ats.interviews /ats/interviews/{id}: get: @@ -5477,7 +5589,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsInterviewOutput' - tags: *ref_51 + tags: *ref_52 x-speakeasy-group: ats.interviews /ats/jobinterviewstages: get: @@ -5526,7 +5638,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedAtsJobinterviewstageOutput - tags: &ref_52 + tags: &ref_53 - ats/jobinterviewstages x-speakeasy-group: ats.jobinterviewstages x-speakeasy-pagination: @@ -5570,7 +5682,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsJobinterviewstageOutput' - tags: *ref_52 + tags: *ref_53 x-speakeasy-group: ats.jobinterviewstages /ats/jobs: get: @@ -5618,7 +5730,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAtsJobOutput' - tags: &ref_53 + tags: &ref_54 - ats/jobs x-speakeasy-group: ats.jobs x-speakeasy-pagination: @@ -5662,7 +5774,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsJobOutput' - tags: *ref_53 + tags: *ref_54 x-speakeasy-group: ats.jobs /ats/offers: get: @@ -5710,7 +5822,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAtsOfferOutput' - tags: &ref_54 + tags: &ref_55 - ats/offers x-speakeasy-group: ats.offers x-speakeasy-pagination: @@ -5754,7 +5866,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsOfferOutput' - tags: *ref_54 + tags: *ref_55 x-speakeasy-group: ats.offers /ats/offices: get: @@ -5802,7 +5914,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAtsOfficeOutput' - tags: &ref_55 + tags: &ref_56 - ats/offices x-speakeasy-group: ats.offices x-speakeasy-pagination: @@ -5846,7 +5958,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsOfficeOutput' - tags: *ref_55 + tags: *ref_56 x-speakeasy-group: ats.offices /ats/rejectreasons: get: @@ -5894,7 +6006,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAtsRejectreasonOutput' - tags: &ref_56 + tags: &ref_57 - ats/rejectreasons x-speakeasy-group: ats.rejectreasons x-speakeasy-pagination: @@ -5938,7 +6050,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsRejectreasonOutput' - tags: *ref_56 + tags: *ref_57 x-speakeasy-group: ats.rejectreasons /ats/scorecards: get: @@ -5986,7 +6098,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAtsScorecardOutput' - tags: &ref_57 + tags: &ref_58 - ats/scorecards x-speakeasy-group: ats.scorecards x-speakeasy-pagination: @@ -6030,7 +6142,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsScorecardOutput' - tags: *ref_57 + tags: *ref_58 x-speakeasy-group: ats.scorecards /ats/tags: get: @@ -6078,7 +6190,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAtsTagOutput' - tags: &ref_58 + tags: &ref_59 - ats/tags x-speakeasy-group: ats.tags x-speakeasy-pagination: @@ -6122,7 +6234,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsTagOutput' - tags: *ref_58 + tags: *ref_59 x-speakeasy-group: ats.tags /ats/users: get: @@ -6170,7 +6282,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAtsUserOutput' - tags: &ref_59 + tags: &ref_60 - ats/users x-speakeasy-group: ats.users x-speakeasy-pagination: @@ -6214,7 +6326,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsUserOutput' - tags: *ref_59 + tags: *ref_60 x-speakeasy-group: ats.users /ats/eeocs: get: @@ -6262,7 +6374,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAtsEeocsOutput' - tags: &ref_60 + tags: &ref_61 - ats/eeocs x-speakeasy-group: ats.eeocs x-speakeasy-pagination: @@ -6304,7 +6416,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAtsEeocsOutput' - tags: *ref_60 + tags: *ref_61 x-speakeasy-group: ats.eeocs /accounting/accounts: get: @@ -6352,7 +6464,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAccountingAccountOutput' - tags: &ref_61 + tags: &ref_62 - accounting/accounts x-speakeasy-group: accounting.accounts x-speakeasy-pagination: @@ -6394,7 +6506,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingAccountOutput' - tags: *ref_61 + tags: *ref_62 x-speakeasy-group: accounting.accounts /accounting/accounts/{id}: get: @@ -6429,7 +6541,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingAccountOutput' - tags: *ref_61 + tags: *ref_62 x-speakeasy-group: accounting.accounts /accounting/addresses: get: @@ -6477,7 +6589,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAccountingAddressOutput' - tags: &ref_62 + tags: &ref_63 - accounting/addresses x-speakeasy-group: accounting.addresses x-speakeasy-pagination: @@ -6521,7 +6633,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingAddressOutput' - tags: *ref_62 + tags: *ref_63 x-speakeasy-group: accounting.addresses /accounting/attachments: get: @@ -6570,7 +6682,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedAccountingAttachmentOutput - tags: &ref_63 + tags: &ref_64 - accounting/attachments x-speakeasy-group: accounting.attachments x-speakeasy-pagination: @@ -6612,7 +6724,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingAttachmentOutput' - tags: *ref_63 + tags: *ref_64 x-speakeasy-group: accounting.attachments /accounting/attachments/{id}: get: @@ -6647,7 +6759,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingAttachmentOutput' - tags: *ref_63 + tags: *ref_64 x-speakeasy-group: accounting.attachments /accounting/balancesheets: get: @@ -6696,7 +6808,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedAccountingBalancesheetOutput - tags: &ref_64 + tags: &ref_65 - accounting/balancesheets x-speakeasy-group: accounting.balancesheets x-speakeasy-pagination: @@ -6740,7 +6852,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingBalancesheetOutput' - tags: *ref_64 + tags: *ref_65 x-speakeasy-group: accounting.balancesheets /accounting/cashflowstatements: get: @@ -6789,7 +6901,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedAccountingCashflowstatementOutput - tags: &ref_65 + tags: &ref_66 - accounting/cashflowstatements x-speakeasy-group: accounting.cashflowstatements x-speakeasy-pagination: @@ -6833,7 +6945,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingCashflowstatementOutput' - tags: *ref_65 + tags: *ref_66 x-speakeasy-group: accounting.cashflowstatements /accounting/companyinfos: get: @@ -6882,7 +6994,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedAccountingCompanyinfoOutput - tags: &ref_66 + tags: &ref_67 - accounting/companyinfos x-speakeasy-group: accounting.companyinfos x-speakeasy-pagination: @@ -6926,7 +7038,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingCompanyinfoOutput' - tags: *ref_66 + tags: *ref_67 x-speakeasy-group: accounting.companyinfos /accounting/contacts: get: @@ -6974,7 +7086,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAccountingContactOutput' - tags: &ref_67 + tags: &ref_68 - accounting/contacts x-speakeasy-group: accounting.contacts x-speakeasy-pagination: @@ -7016,7 +7128,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingContactOutput' - tags: *ref_67 + tags: *ref_68 x-speakeasy-group: accounting.contacts /accounting/contacts/{id}: get: @@ -7051,7 +7163,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingContactOutput' - tags: *ref_67 + tags: *ref_68 x-speakeasy-group: accounting.contacts /accounting/creditnotes: get: @@ -7100,7 +7212,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedAccountingCreditnoteOutput - tags: &ref_68 + tags: &ref_69 - accounting/creditnotes x-speakeasy-group: accounting.creditnotes x-speakeasy-pagination: @@ -7144,7 +7256,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingCreditnoteOutput' - tags: *ref_68 + tags: *ref_69 x-speakeasy-group: accounting.creditnotes /accounting/expenses: get: @@ -7192,7 +7304,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAccountingExpenseOutput' - tags: &ref_69 + tags: &ref_70 - accounting/expenses x-speakeasy-group: accounting.expenses x-speakeasy-pagination: @@ -7234,7 +7346,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingExpenseOutput' - tags: *ref_69 + tags: *ref_70 x-speakeasy-group: accounting.expenses /accounting/expenses/{id}: get: @@ -7269,7 +7381,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingExpenseOutput' - tags: *ref_69 + tags: *ref_70 x-speakeasy-group: accounting.expenses /accounting/incomestatements: get: @@ -7318,7 +7430,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedAccountingIncomestatementOutput - tags: &ref_70 + tags: &ref_71 - accounting/incomestatements x-speakeasy-group: accounting.incomestatements x-speakeasy-pagination: @@ -7362,7 +7474,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingIncomestatementOutput' - tags: *ref_70 + tags: *ref_71 x-speakeasy-group: accounting.incomestatements /accounting/invoices: get: @@ -7410,7 +7522,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAccountingInvoiceOutput' - tags: &ref_71 + tags: &ref_72 - accounting/invoices x-speakeasy-group: accounting.invoices x-speakeasy-pagination: @@ -7452,7 +7564,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingInvoiceOutput' - tags: *ref_71 + tags: *ref_72 x-speakeasy-group: accounting.invoices /accounting/invoices/{id}: get: @@ -7487,7 +7599,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingInvoiceOutput' - tags: *ref_71 + tags: *ref_72 x-speakeasy-group: accounting.invoices /accounting/items: get: @@ -7535,7 +7647,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAccountingItemOutput' - tags: &ref_72 + tags: &ref_73 - accounting/items x-speakeasy-group: accounting.items x-speakeasy-pagination: @@ -7579,7 +7691,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingItemOutput' - tags: *ref_72 + tags: *ref_73 x-speakeasy-group: accounting.items /accounting/journalentries: get: @@ -7628,7 +7740,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedAccountingJournalentryOutput - tags: &ref_73 + tags: &ref_74 - accounting/journalentries x-speakeasy-group: accounting.journalentries x-speakeasy-pagination: @@ -7670,7 +7782,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingJournalentryOutput' - tags: *ref_73 + tags: *ref_74 x-speakeasy-group: accounting.journalentries /accounting/journalentries/{id}: get: @@ -7705,7 +7817,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingJournalentryOutput' - tags: *ref_73 + tags: *ref_74 x-speakeasy-group: accounting.journalentries /accounting/payments: get: @@ -7753,7 +7865,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAccountingPaymentOutput' - tags: &ref_74 + tags: &ref_75 - accounting/payments x-speakeasy-group: accounting.payments x-speakeasy-pagination: @@ -7795,7 +7907,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingPaymentOutput' - tags: *ref_74 + tags: *ref_75 x-speakeasy-group: accounting.payments /accounting/payments/{id}: get: @@ -7830,7 +7942,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingPaymentOutput' - tags: *ref_74 + tags: *ref_75 x-speakeasy-group: accounting.payments /accounting/phonenumbers: get: @@ -7879,7 +7991,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedAccountingPhonenumberOutput - tags: &ref_75 + tags: &ref_76 - accounting/phonenumbers x-speakeasy-group: accounting.phonenumbers x-speakeasy-pagination: @@ -7923,7 +8035,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingPhonenumberOutput' - tags: *ref_75 + tags: *ref_76 x-speakeasy-group: accounting.phonenumbers /accounting/purchaseorders: get: @@ -7972,7 +8084,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedAccountingPurchaseorderOutput - tags: &ref_76 + tags: &ref_77 - accounting/purchaseorders x-speakeasy-group: accounting.purchaseorders x-speakeasy-pagination: @@ -8014,7 +8126,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingPurchaseorderOutput' - tags: *ref_76 + tags: *ref_77 x-speakeasy-group: accounting.purchaseorders /accounting/purchaseorders/{id}: get: @@ -8049,7 +8161,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingPurchaseorderOutput' - tags: *ref_76 + tags: *ref_77 x-speakeasy-group: accounting.purchaseorders /accounting/taxrates: get: @@ -8097,7 +8209,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedAccountingTaxrateOutput' - tags: &ref_77 + tags: &ref_78 - accounting/taxrates x-speakeasy-group: accounting.taxrates x-speakeasy-pagination: @@ -8141,7 +8253,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingTaxrateOutput' - tags: *ref_77 + tags: *ref_78 x-speakeasy-group: accounting.taxrates /accounting/trackingcategories: get: @@ -8190,7 +8302,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedAccountingTrackingcategoryOutput - tags: &ref_78 + tags: &ref_79 - accounting/trackingcategories x-speakeasy-group: accounting.trackingcategories x-speakeasy-pagination: @@ -8234,7 +8346,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingTrackingcategoryOutput' - tags: *ref_78 + tags: *ref_79 x-speakeasy-group: accounting.trackingcategories /accounting/transactions: get: @@ -8283,7 +8395,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedAccountingTransactionOutput - tags: &ref_79 + tags: &ref_80 - accounting/transactions x-speakeasy-group: accounting.transactions x-speakeasy-pagination: @@ -8327,7 +8439,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingTransactionOutput' - tags: *ref_79 + tags: *ref_80 x-speakeasy-group: accounting.transactions /accounting/vendorcredits: get: @@ -8376,7 +8488,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedAccountingVendorcreditOutput - tags: &ref_80 + tags: &ref_81 - accounting/vendorcredits x-speakeasy-group: accounting.vendorcredits x-speakeasy-pagination: @@ -8420,7 +8532,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAccountingVendorcreditOutput' - tags: *ref_80 + tags: *ref_81 x-speakeasy-group: accounting.vendorcredits /filestorage/drives: get: @@ -8468,7 +8580,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedFilestorageDriveOutput' - tags: &ref_81 + tags: &ref_82 - filestorage/drives x-speakeasy-group: filestorage.drives x-speakeasy-pagination: @@ -8512,7 +8624,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedFilestorageDriveOutput' - tags: *ref_81 + tags: *ref_82 x-speakeasy-group: filestorage.drives /filestorage/files: get: @@ -8560,7 +8672,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedFilestorageFileOutput' - tags: &ref_82 + tags: &ref_83 - filestorage/files x-speakeasy-group: filestorage.files x-speakeasy-pagination: @@ -8602,7 +8714,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedFilestorageFileOutput' - tags: *ref_82 + tags: *ref_83 x-speakeasy-group: filestorage.files /filestorage/files/{id}: get: @@ -8637,7 +8749,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedFilestorageFileOutput' - tags: *ref_82 + tags: *ref_83 x-speakeasy-group: filestorage.files /filestorage/folders: get: @@ -8685,7 +8797,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedFilestorageFolderOutput' - tags: &ref_83 + tags: &ref_84 - filestorage/folders x-speakeasy-group: filestorage.folders x-speakeasy-pagination: @@ -8727,7 +8839,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedFilestorageFolderOutput' - tags: *ref_83 + tags: *ref_84 x-speakeasy-group: filestorage.folders /filestorage/folders/{id}: get: @@ -8762,7 +8874,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedFilestorageFolderOutput' - tags: *ref_83 + tags: *ref_84 x-speakeasy-group: filestorage.folders /filestorage/groups: get: @@ -8810,7 +8922,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedFilestorageGroupOutput' - tags: &ref_84 + tags: &ref_85 - filestorage/groups x-speakeasy-group: filestorage.groups x-speakeasy-pagination: @@ -8854,7 +8966,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedFilestorageGroupOutput' - tags: *ref_84 + tags: *ref_85 x-speakeasy-group: filestorage.groups /filestorage/users: get: @@ -8902,7 +9014,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedFilestorageUserOutput' - tags: &ref_85 + tags: &ref_86 - filestorage/users x-speakeasy-group: filestorage.users x-speakeasy-pagination: @@ -8946,7 +9058,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedFilestorageUserOutput' - tags: *ref_85 + tags: *ref_86 x-speakeasy-group: filestorage.users /ecommerce/products: get: @@ -8994,7 +9106,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedEcommerceProductOutput' - tags: &ref_86 + tags: &ref_87 - ecommerce/products x-speakeasy-group: ecommerce.products x-speakeasy-pagination: @@ -9036,7 +9148,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedEcommerceProductOutput' - tags: *ref_86 + tags: *ref_87 x-speakeasy-group: ecommerce.products /ecommerce/products/{id}: get: @@ -9069,7 +9181,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedEcommerceProductOutput' - tags: *ref_86 + tags: *ref_87 x-speakeasy-group: ecommerce.products /ecommerce/orders: get: @@ -9117,7 +9229,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedEcommerceOrderOutput' - tags: &ref_87 + tags: &ref_88 - ecommerce/orders x-speakeasy-group: ecommerce.orders x-speakeasy-pagination: @@ -9159,7 +9271,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedEcommerceOrderOutput' - tags: *ref_87 + tags: *ref_88 x-speakeasy-group: ecommerce.orders /ecommerce/orders/{id}: get: @@ -9192,7 +9304,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedEcommerceOrderOutput' - tags: *ref_87 + tags: *ref_88 x-speakeasy-group: ecommerce.orders /ecommerce/customers: get: @@ -9240,7 +9352,7 @@ paths: type: array items: $ref: '#/components/schemas/UnifiedEcommerceCustomerOutput' - tags: &ref_88 + tags: &ref_89 - ecommerce/customers x-speakeasy-group: ecommerce.customers x-speakeasy-pagination: @@ -9282,7 +9394,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedEcommerceCustomerOutput' - tags: *ref_88 + tags: *ref_89 x-speakeasy-group: ecommerce.customers /ecommerce/fulfillments: get: @@ -9331,7 +9443,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedEcommerceFulfillmentOutput - tags: &ref_89 + tags: &ref_90 - ecommerce/fulfillments x-speakeasy-group: ecommerce.fulfillments x-speakeasy-pagination: @@ -9373,7 +9485,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedEcommerceFulfillmentOutput' - tags: *ref_89 + tags: *ref_90 x-speakeasy-group: ecommerce.fulfillments /ticketing/attachments: get: @@ -9422,7 +9534,7 @@ paths: items: $ref: >- #/components/schemas/UnifiedTicketingAttachmentOutput - tags: &ref_90 + tags: &ref_91 - ticketing/attachments x-speakeasy-group: ticketing.attachments x-speakeasy-pagination: @@ -9463,7 +9575,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedTicketingAttachmentOutput' - tags: *ref_90 + tags: *ref_91 x-speakeasy-group: ticketing.attachments /ticketing/attachments/{id}: get: @@ -9498,7 +9610,7 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedTicketingAttachmentOutput' - tags: *ref_90 + tags: *ref_91 x-speakeasy-group: ticketing.attachments info: title: Panora API @@ -9752,7 +9864,7 @@ components: type: string nullable: true example: USER - enum: &ref_120 + enum: &ref_121 - USER - CONTACT description: >- @@ -9779,12 +9891,12 @@ components: specified) attachments: type: array - items: &ref_121 + items: &ref_122 oneOf: - type: string - $ref: '#/components/schemas/UnifiedTicketingAttachmentOutput' nullable: true - example: &ref_122 + example: &ref_123 - 801f9ede-c698-4e66-a7fc-48d19eebaa4f description: The attachements UUIDs tied to the comment required: @@ -9800,7 +9912,7 @@ components: status: type: string example: OPEN - enum: &ref_91 + enum: &ref_92 - OPEN - CLOSED nullable: true @@ -9819,7 +9931,7 @@ components: type: type: string example: BUG - enum: &ref_92 + enum: &ref_93 - BUG - SUBTASK - TASK @@ -9835,21 +9947,21 @@ components: description: The UUID of the parent ticket collections: type: array - items: &ref_93 + items: &ref_94 oneOf: - type: string - $ref: '#/components/schemas/UnifiedTicketingCollectionOutput' - example: &ref_94 + example: &ref_95 - 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true description: The collection UUIDs the ticket belongs to tags: type: array - items: &ref_95 + items: &ref_96 oneOf: - type: string - $ref: '#/components/schemas/UnifiedTicketingTagOutput' - example: &ref_96 + example: &ref_97 - my_tag - urgent_tag nullable: true @@ -9863,7 +9975,7 @@ components: priority: type: string example: HIGH - enum: &ref_97 + enum: &ref_98 - HIGH - MEDIUM - LOW @@ -9872,7 +9984,7 @@ components: The priority of the ticket. Authorized values are HIGH, MEDIUM or LOW. assigned_to: - example: &ref_98 + example: &ref_99 - 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true description: The users UUIDs the ticket is assigned to @@ -9880,7 +9992,7 @@ components: items: type: string comment: - example: &ref_99 + example: &ref_100 content: Assigned the issue ! nullable: true description: The comment of the ticket @@ -9898,17 +10010,17 @@ components: description: The UUID of the contact which the ticket belongs to attachments: type: array - items: &ref_100 + items: &ref_101 oneOf: - type: string - $ref: '#/components/schemas/UnifiedTicketingAttachmentInput' - example: &ref_101 + example: &ref_102 - 801f9ede-c698-4e66-a7fc-48d19eebaa4f description: The attachements UUIDs tied to the ticket nullable: true field_mappings: type: object - example: &ref_102 + example: &ref_103 fav_dish: broccoli fav_color: red nullable: true @@ -9961,7 +10073,7 @@ components: status: type: string example: OPEN - enum: *ref_91 + enum: *ref_92 nullable: true description: The status of the ticket. Authorized values are OPEN or CLOSED. description: @@ -9978,7 +10090,7 @@ components: type: type: string example: BUG - enum: *ref_92 + enum: *ref_93 nullable: true description: >- The type of the ticket. Authorized values are PROBLEM, QUESTION, or @@ -9990,14 +10102,14 @@ components: description: The UUID of the parent ticket collections: type: array - items: *ref_93 - example: *ref_94 + items: *ref_94 + example: *ref_95 nullable: true description: The collection UUIDs the ticket belongs to tags: type: array - items: *ref_95 - example: *ref_96 + items: *ref_96 + example: *ref_97 nullable: true description: The tags names of the ticket completed_at: @@ -10009,20 +10121,20 @@ components: priority: type: string example: HIGH - enum: *ref_97 + enum: *ref_98 nullable: true description: >- The priority of the ticket. Authorized values are HIGH, MEDIUM or LOW. assigned_to: - example: *ref_98 + example: *ref_99 nullable: true description: The users UUIDs the ticket is assigned to type: array items: type: string comment: - example: *ref_99 + example: *ref_100 nullable: true description: The comment of the ticket allOf: @@ -10039,13 +10151,13 @@ components: description: The UUID of the contact which the ticket belongs to attachments: type: array - items: *ref_100 - example: *ref_101 + items: *ref_101 + example: *ref_102 description: The attachements UUIDs tied to the ticket nullable: true field_mappings: type: object - example: *ref_102 + example: *ref_103 nullable: true description: >- The custom field mappings of the ticket between the remote 3rd party @@ -10400,7 +10512,7 @@ components: industry: type: string example: ACCOUNTING - enum: &ref_103 + enum: &ref_104 - ACCOUNTING - AIRLINES_AVIATION - ALTERNATIVE_DISPUTE_RESOLUTION @@ -10564,7 +10676,7 @@ components: nullable: true email_addresses: description: The email addresses of the company - example: &ref_104 + example: &ref_105 - email_address: acme@gmail.com email_address_type: WORK nullable: true @@ -10573,7 +10685,7 @@ components: $ref: '#/components/schemas/Email' addresses: description: The addresses of the company - example: &ref_105 + example: &ref_106 - street_1: 5th Avenue city: New York state: NY @@ -10585,7 +10697,7 @@ components: $ref: '#/components/schemas/Address' phone_numbers: description: The phone numbers of the company - example: &ref_106 + example: &ref_107 - phone_number: '+33660606067' phone_type: WORK nullable: true @@ -10594,7 +10706,7 @@ components: $ref: '#/components/schemas/Phone' field_mappings: type: object - example: &ref_107 + example: &ref_108 fav_dish: broccoli fav_color: red description: >- @@ -10643,7 +10755,7 @@ components: industry: type: string example: ACCOUNTING - enum: *ref_103 + enum: *ref_104 description: >- The industry of the company. Authorized values can be found in the Industry enum. @@ -10660,28 +10772,28 @@ components: nullable: true email_addresses: description: The email addresses of the company - example: *ref_104 + example: *ref_105 nullable: true type: array items: $ref: '#/components/schemas/Email' addresses: description: The addresses of the company - example: *ref_105 + example: *ref_106 nullable: true type: array items: $ref: '#/components/schemas/Address' phone_numbers: description: The phone numbers of the company - example: *ref_106 + example: *ref_107 nullable: true type: array items: $ref: '#/components/schemas/Phone' field_mappings: type: object - example: *ref_107 + example: *ref_108 description: >- The custom field mappings of the company between the remote 3rd party & Panora @@ -10705,7 +10817,7 @@ components: email_addresses: nullable: true description: The email addresses of the contact - example: &ref_108 + example: &ref_109 - email: john.doe@example.com type: WORK type: array @@ -10714,7 +10826,7 @@ components: phone_numbers: nullable: true description: The phone numbers of the contact - example: &ref_109 + example: &ref_110 - phone: '1234567890' type: WORK type: array @@ -10723,7 +10835,7 @@ components: addresses: nullable: true description: The addresses of the contact - example: &ref_110 + example: &ref_111 - street: 123 Main St city: Anytown state: CA @@ -10740,7 +10852,7 @@ components: example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f field_mappings: type: object - example: &ref_111 + example: &ref_112 fav_dish: broccoli fav_color: red nullable: true @@ -10797,21 +10909,21 @@ components: email_addresses: nullable: true description: The email addresses of the contact - example: *ref_108 + example: *ref_109 type: array items: $ref: '#/components/schemas/Email' phone_numbers: nullable: true description: The phone numbers of the contact - example: *ref_109 + example: *ref_110 type: array items: $ref: '#/components/schemas/Phone' addresses: nullable: true description: The addresses of the contact - example: *ref_110 + example: *ref_111 type: array items: $ref: '#/components/schemas/Address' @@ -10822,7 +10934,7 @@ components: example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f field_mappings: type: object - example: *ref_111 + example: *ref_112 nullable: true description: >- The custom field mappings of the contact between the remote 3rd @@ -10867,7 +10979,7 @@ components: field_mappings: type: object nullable: true - example: &ref_112 + example: &ref_113 fav_dish: broccoli fav_color: red description: >- @@ -10944,7 +11056,7 @@ components: field_mappings: type: object nullable: true - example: *ref_112 + example: *ref_113 description: >- The custom field mappings of the company between the remote 3rd party & Panora @@ -10965,7 +11077,7 @@ components: type: string nullable: true example: INBOUND - enum: &ref_113 + enum: &ref_114 - INBOUND - OUTBOUND description: >- @@ -10992,7 +11104,7 @@ components: type: string nullable: true example: MEETING - enum: &ref_114 + enum: &ref_115 - EMAIL - CALL - MEETING @@ -11011,7 +11123,7 @@ components: description: The UUID of the company tied to the engagement contacts: nullable: true - example: &ref_115 + example: &ref_116 - 801f9ede-c698-4e66-a7fc-48d19eebaa4f description: The UUIDs of contacts tied to the engagement object type: array @@ -11020,7 +11132,7 @@ components: field_mappings: type: object nullable: true - example: &ref_116 + example: &ref_117 fav_dish: broccoli fav_color: red description: >- @@ -11073,7 +11185,7 @@ components: type: string nullable: true example: INBOUND - enum: *ref_113 + enum: *ref_114 description: >- The direction of the engagement. Authorized values are INBOUND or OUTBOUND @@ -11098,7 +11210,7 @@ components: type: string nullable: true example: MEETING - enum: *ref_114 + enum: *ref_115 description: >- The type of the engagement. Authorized values are EMAIL, CALL or MEETING @@ -11114,7 +11226,7 @@ components: description: The UUID of the company tied to the engagement contacts: nullable: true - example: *ref_115 + example: *ref_116 description: The UUIDs of contacts tied to the engagement object type: array items: @@ -11122,7 +11234,7 @@ components: field_mappings: type: object nullable: true - example: *ref_116 + example: *ref_117 description: >- The custom field mappings of the engagement between the remote 3rd party & Panora @@ -11159,7 +11271,7 @@ components: description: The UUID of the deal tied to the note field_mappings: type: object - example: &ref_117 + example: &ref_118 fav_dish: broccoli fav_color: red nullable: true @@ -11229,7 +11341,7 @@ components: description: The UUID of the deal tied to the note field_mappings: type: object - example: *ref_117 + example: *ref_118 nullable: true description: >- The custom field mappings of the note between the remote 3rd party & @@ -11301,7 +11413,7 @@ components: status: type: string example: PENDING - enum: &ref_118 + enum: &ref_119 - PENDING - COMPLETED description: The status of the task. Authorized values are PENDING, COMPLETED. @@ -11333,7 +11445,7 @@ components: nullable: true field_mappings: type: object - example: &ref_119 + example: &ref_120 fav_dish: broccoli fav_color: red description: >- @@ -11392,7 +11504,7 @@ components: status: type: string example: PENDING - enum: *ref_118 + enum: *ref_119 description: The status of the task. Authorized values are PENDING, COMPLETED. nullable: true due_date: @@ -11422,7 +11534,7 @@ components: nullable: true field_mappings: type: object - example: *ref_119 + example: *ref_120 description: >- The custom field mappings of the task between the remote 3rd party & Panora @@ -11565,7 +11677,7 @@ components: type: string nullable: true example: USER - enum: *ref_120 + enum: *ref_121 description: >- The creator type of the comment. Authorized values are either USER or CONTACT @@ -11590,9 +11702,9 @@ components: specified) attachments: type: array - items: *ref_121 + items: *ref_122 nullable: true - example: *ref_122 + example: *ref_123 description: The attachements UUIDs tied to the comment id: type: string @@ -12221,148 +12333,134 @@ components: - path UnifiedHrisBankinfoOutput: type: object - properties: {} + properties: + account_type: + type: string + example: CHECKING + enum: + - SAVINGS + - CHECKING + nullable: true + description: The type of the bank account + bank_name: + type: string + example: Bank of America + nullable: true + description: The name of the bank + account_number: + type: string + example: '1234567890' + nullable: true + description: The account number + routing_number: + type: string + example: '021000021' + nullable: true + description: The routing number of the bank + employee_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated employee + field_mappings: + type: object + example: + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the bank info record + remote_id: + type: string + example: id_1 + nullable: true + description: The remote ID of the bank info in the context of the 3rd Party + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: The remote data of the bank info in the context of the 3rd Party + remote_created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The date when the bank info was created in the 3rd party system + created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The created date of the bank info record + modified_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The last modified date of the bank info record + remote_was_deleted: + type: boolean + example: false + nullable: true + description: Indicates if the bank info was deleted in the remote system + required: + - id + - created_at + - modified_at + - remote_was_deleted UnifiedHrisBenefitOutput: - type: object - properties: {} - UnifiedHrisCompanyOutput: - type: object - properties: {} - UnifiedHrisDependentOutput: - type: object - properties: {} - UnifiedHrisEmployeepayrollrunOutput: - type: object - properties: {} - UnifiedHrisEmployeeOutput: - type: object - properties: {} - UnifiedHrisEmployeeInput: - type: object - properties: {} - UnifiedHrisEmployerbenefitOutput: - type: object - properties: {} - UnifiedHrisEmploymentOutput: - type: object - properties: {} - UnifiedHrisGroupOutput: - type: object - properties: {} - UnifiedHrisLocationOutput: - type: object - properties: {} - UnifiedHrisPaygroupOutput: - type: object - properties: {} - UnifiedHrisPayrollrunOutput: - type: object - properties: {} - UnifiedHrisTimeoffOutput: - type: object - properties: {} - UnifiedHrisTimeoffInput: - type: object - properties: {} - UnifiedHrisTimeoffbalanceOutput: - type: object - properties: {} - UnifiedMarketingautomationActionOutput: - type: object - properties: {} - UnifiedMarketingautomationActionInput: - type: object - properties: {} - UnifiedMarketingautomationAutomationOutput: - type: object - properties: {} - UnifiedMarketingautomationAutomationInput: - type: object - properties: {} - UnifiedMarketingautomationCampaignOutput: - type: object - properties: {} - UnifiedMarketingautomationCampaignInput: - type: object - properties: {} - UnifiedMarketingautomationContactOutput: - type: object - properties: {} - UnifiedMarketingautomationContactInput: - type: object - properties: {} - UnifiedMarketingautomationEmailOutput: - type: object - properties: {} - UnifiedMarketingautomationEventOutput: - type: object - properties: {} - UnifiedMarketingautomationListOutput: - type: object - properties: {} - UnifiedMarketingautomationListInput: - type: object - properties: {} - UnifiedMarketingautomationMessageOutput: - type: object - properties: {} - UnifiedMarketingautomationTemplateOutput: - type: object - properties: {} - UnifiedMarketingautomationTemplateInput: - type: object - properties: {} - UnifiedMarketingautomationUserOutput: - type: object - properties: {} - UnifiedAtsActivityOutput: type: object properties: - activity_type: + provider_name: type: string - enum: &ref_123 - - NOTE - - EMAIL - - OTHER - example: NOTE + example: Health Insurance Provider nullable: true - description: The type of activity - subject: + description: The name of the benefit provider + employee_id: type: string - example: Email subject + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The subject of the activity - body: - type: string - example: Dear Diana, I love you + description: The UUID of the associated employee + employee_contribution: + type: number + example: 100 nullable: true - description: The body of the activity - visibility: + description: The employee contribution amount + company_contribution: + type: number + example: 200 + nullable: true + description: The company contribution amount + start_date: + format: date-time type: string - enum: &ref_124 - - ADMIN_ONLY - - PUBLIC - - PRIVATE - example: PUBLIC + example: '2024-01-01T00:00:00Z' nullable: true - description: The visibility of the activity - candidate_id: + description: The start date of the benefit + end_date: + format: date-time type: string - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + example: '2024-12-31T23:59:59Z' nullable: true - description: The UUID of the candidate - remote_created_at: + description: The end date of the benefit + employer_benefit_id: type: string - format: date-time - example: '2024-10-01T12:00:00Z' + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The remote creation date of the activity + description: The UUID of the associated employer benefit field_mappings: type: object - example: &ref_125 - fav_dish: broccoli - fav_color: red - additionalProperties: true + example: + custom_field_1: value1 + custom_field_2: value2 nullable: true description: >- The custom field mappings of the object between the remote 3rd party @@ -12371,284 +12469,195 @@ components: type: string example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The UUID of the activity + description: The UUID of the benefit record remote_id: type: string - example: id_1 + example: benefit_1234 nullable: true - description: The remote ID of the activity in the context of the 3rd Party + description: The remote ID of the benefit in the context of the 3rd Party remote_data: type: object example: - fav_dish: broccoli - fav_color: red + raw_data: + additional_field: some value nullable: true - additionalProperties: true - description: The remote data of the activity in the context of the 3rd Party - created_at: + description: The remote data of the benefit in the context of the 3rd Party + remote_created_at: format: date-time type: string example: '2024-10-01T12:00:00Z' nullable: true - description: The created date of the object - modified_at: + description: The date when the benefit was created in the 3rd party system + created_at: format: date-time type: string example: '2024-10-01T12:00:00Z' nullable: true - description: The modified date of the object - UnifiedAtsActivityInput: - type: object - properties: - activity_type: - type: string - enum: *ref_123 - example: NOTE - nullable: true - description: The type of activity - subject: - type: string - example: Email subject - nullable: true - description: The subject of the activity - body: - type: string - example: Dear Diana, I love you - nullable: true - description: The body of the activity - visibility: - type: string - enum: *ref_124 - example: PUBLIC - nullable: true - description: The visibility of the activity - candidate_id: - type: string - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - nullable: true - description: The UUID of the candidate - remote_created_at: - type: string + description: The created date of the benefit record + modified_at: format: date-time + type: string example: '2024-10-01T12:00:00Z' nullable: true - description: The remote creation date of the activity - field_mappings: - type: object - example: *ref_125 - additionalProperties: true + description: The last modified date of the benefit record + remote_was_deleted: + type: boolean + example: false nullable: true - description: >- - The custom field mappings of the object between the remote 3rd party - & Panora - UnifiedAtsApplicationOutput: + description: Indicates if the benefit was deleted in the remote system + UnifiedHrisCompanyOutput: type: object properties: - applied_at: - format: date-time - type: string - nullable: true - description: The application date - example: '2024-10-01T12:00:00Z' - rejected_at: - format: date-time + legal_name: type: string + example: Acme Corporation nullable: true - description: The rejection date - example: '2024-10-01T12:00:00Z' - offers: - nullable: true - description: The offers UUIDs for the application - example: &ref_126 + description: The legal name of the company + locations: + example: - 801f9ede-c698-4e66-a7fc-48d19eebaa4f - - 12345678-1234-1234-1234-123456789012 + nullable: true + description: UUIDs of the of the Location associated with the company type: array items: type: string - source: - type: string - nullable: true - description: The source of the application - example: Source Name - credited_to: - type: string - nullable: true - description: The UUID of the person credited for the application - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - current_stage: - type: string - nullable: true - description: The UUID of the current stage of the application - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - reject_reason: + display_name: type: string + example: Acme Corp nullable: true - description: The rejection reason for the application - example: Candidate not experienced enough - candidate_id: - type: string + description: The display name of the company + eins: + example: + - 12-3456789 + - 98-7654321 nullable: true - description: The UUID of the candidate - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - job_id: - type: string - description: The UUID of the job - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + description: The Employer Identification Numbers (EINs) of the company + type: array + items: + type: string field_mappings: type: object - example: &ref_127 - fav_dish: broccoli - fav_color: red - additionalProperties: true + example: + custom_field_1: value1 + custom_field_2: value2 nullable: true description: >- The custom field mappings of the object between the remote 3rd party & Panora id: type: string - nullable: true - description: The UUID of the application example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the company record remote_id: type: string + example: company_1234 nullable: true - description: The remote ID of the application in the context of the 3rd Party - example: id_1 + description: The remote ID of the company in the context of the 3rd Party remote_data: type: object example: - fav_dish: broccoli - fav_color: red + raw_data: + additional_field: some value nullable: true - additionalProperties: true - description: The remote data of the application in the context of the 3rd Party - created_at: + description: The remote data of the company in the context of the 3rd Party + remote_created_at: format: date-time type: string example: '2024-10-01T12:00:00Z' nullable: true - description: The created date of the object - modified_at: + description: The date when the company was created in the 3rd party system + created_at: format: date-time type: string example: '2024-10-01T12:00:00Z' nullable: true - description: The modified date of the object - remote_created_at: + description: The created date of the company record + modified_at: format: date-time type: string + example: '2024-10-01T12:00:00Z' nullable: true - description: The remote created date of the object - remote_modified_at: - format: date-time - type: string + description: The last modified date of the company record + remote_was_deleted: + type: boolean + example: false nullable: true - description: The remote modified date of the object - UnifiedAtsApplicationInput: + description: Indicates if the company was deleted in the remote system + UnifiedHrisDependentOutput: type: object properties: - applied_at: - format: date-time + first_name: type: string + example: John nullable: true - description: The application date - example: '2024-10-01T12:00:00Z' - rejected_at: - format: date-time + description: The first name of the dependent + last_name: type: string + example: Doe nullable: true - description: The rejection date - example: '2024-10-01T12:00:00Z' - offers: - nullable: true - description: The offers UUIDs for the application - example: *ref_126 - type: array - items: - type: string - source: + description: The last name of the dependent + middle_name: type: string + example: Michael nullable: true - description: The source of the application - example: Source Name - credited_to: + description: The middle name of the dependent + relationship: type: string + example: CHILD + enum: + - CHILD + - SPOUSE + - DOMESTIC_PARTNER nullable: true - description: The UUID of the person credited for the application - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - current_stage: + description: The relationship of the dependent to the employee + date_of_birth: + format: date-time type: string + example: '2020-01-01' nullable: true - description: The UUID of the current stage of the application - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - reject_reason: + description: The date of birth of the dependent + gender: type: string + example: MALE + enum: + - MALE + - FEMALE + - NON-BINARY + - OTHER + - PREFER_NOT_TO_DISCLOSE nullable: true - description: The rejection reason for the application - example: Candidate not experienced enough - candidate_id: + description: The gender of the dependent + phone_number: type: string + example: '+1234567890' nullable: true - description: The UUID of the candidate - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - job_id: + description: The phone number of the dependent + home_location: type: string - description: The UUID of the job example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - field_mappings: - type: object - example: *ref_127 - additionalProperties: true - nullable: true - description: >- - The custom field mappings of the object between the remote 3rd party - & Panora - UnifiedAtsAttachmentOutput: - type: object - properties: - file_url: - type: string - example: https://example.com/file.pdf - nullable: true - description: The URL of the file - file_name: - type: string - example: file.pdf - nullable: true - description: The name of the file - attachment_type: - type: string - example: RESUME - enum: &ref_128 - - RESUME - - COVER_LETTER - - OFFER_LETTER - - OTHER nullable: true - description: The type of the file - remote_created_at: - type: string - example: '2024-10-01T12:00:00Z' - format: date-time + description: The UUID of the home location + is_student: + type: boolean + example: true nullable: true - description: The remote creation date of the attachment - remote_modified_at: + description: Indicates if the dependent is a student + ssn: type: string - example: '2024-10-01T12:00:00Z' - format: date-time + example: 123-45-6789 nullable: true - description: The remote modification date of the attachment - candidate_id: + description: The Social Security Number of the dependent + employee_id: type: string example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The UUID of the candidate + description: The UUID of the associated employee field_mappings: type: object - example: &ref_129 - fav_dish: broccoli - fav_color: red - additionalProperties: true + example: + custom_field_1: value1 + custom_field_2: value2 nullable: true description: >- The custom field mappings of the object between the remote 3rd party @@ -12657,212 +12666,369 @@ components: type: string example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The UUID of the attachment + description: The UUID of the dependent record remote_id: type: string - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + example: dependent_1234 nullable: true - description: The remote ID of the attachment + description: The remote ID of the dependent in the context of the 3rd Party remote_data: type: object example: - fav_dish: broccoli - fav_color: red + raw_data: + additional_field: some value nullable: true - additionalProperties: true - description: The remote data of the attachment in the context of the 3rd Party + description: The remote data of the dependent in the context of the 3rd Party + remote_created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The date when the dependent was created in the 3rd party system created_at: format: date-time type: string example: '2024-10-01T12:00:00Z' nullable: true - description: The created date of the object + description: The created date of the dependent record modified_at: format: date-time type: string example: '2024-10-01T12:00:00Z' nullable: true - description: The modified date of the object - UnifiedAtsAttachmentInput: + description: The last modified date of the dependent record + remote_was_deleted: + type: boolean + example: false + nullable: true + description: Indicates if the dependent was deleted in the remote system + DeductionItem: type: object properties: - file_url: + name: type: string - example: https://example.com/file.pdf + example: Health Insurance nullable: true - description: The URL of the file - file_name: - type: string - example: file.pdf + description: The name of the deduction + employee_deduction: + type: number + example: 100 nullable: true - description: The name of the file - attachment_type: - type: string - example: RESUME - enum: *ref_128 + description: The amount of employee deduction + company_deduction: + type: number + example: 200 nullable: true - description: The type of the file - remote_created_at: - type: string - example: '2024-10-01T12:00:00Z' - format: date-time + description: The amount of company deduction + EarningItem: + type: object + properties: + amount: + type: number + example: 1000 nullable: true - description: The remote creation date of the attachment - remote_modified_at: + description: The amount of the earning + type: type: string - example: '2024-10-01T12:00:00Z' - format: date-time + example: Salary nullable: true - description: The remote modification date of the attachment - candidate_id: + description: The type of the earning + TaxItem: + type: object + properties: + name: type: string - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + example: Federal Income Tax nullable: true - description: The UUID of the candidate - field_mappings: - type: object - example: *ref_129 - additionalProperties: true + description: The name of the tax + amount: + type: number + example: 250 nullable: true - description: >- - The custom field mappings of the object between the remote 3rd party - & Panora - Url: + description: The amount of the tax + employer_tax: + type: boolean + example: true + nullable: true + description: Indicates if this is an employer tax + UnifiedHrisEmployeepayrollrunOutput: type: object properties: - url: + employee_id: type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The url. - url_type: + description: The UUID of the associated employee + payroll_run_id: type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The url type. It takes [WEBSITE | BLOG | LINKEDIN | GITHUB | OTHER] - required: - - url - - url_type - UnifiedAtsCandidateOutput: - type: object - properties: - first_name: - type: string - example: Joe + description: The UUID of the associated payroll run + gross_pay: + type: number + example: 5000 nullable: true - description: The first name of the candidate - last_name: + description: The gross pay amount + net_pay: + type: number + example: 4000 + nullable: true + description: The net pay amount + start_date: + format: date-time type: string - example: Doe + example: '2023-01-01T00:00:00Z' nullable: true - description: The last name of the candidate - company: + description: The start date of the pay period + end_date: + format: date-time type: string - example: Acme + example: '2023-01-15T23:59:59Z' nullable: true - description: The company of the candidate - title: + description: The end date of the pay period + check_date: + format: date-time type: string - example: Analyst + example: '2023-01-20T00:00:00Z' nullable: true - description: The title of the candidate - locations: + description: The date the check was issued + deductions: + nullable: true + description: The list of deductions for this payroll run + type: array + items: + $ref: '#/components/schemas/DeductionItem' + earnings: + nullable: true + description: The list of earnings for this payroll run + type: array + items: + $ref: '#/components/schemas/EarningItem' + taxes: + nullable: true + description: The list of taxes for this payroll run + type: array + items: + $ref: '#/components/schemas/TaxItem' + field_mappings: + type: object + example: + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: type: string - example: New York + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The locations of the candidate - is_private: - type: boolean - example: false + description: The UUID of the employee payroll run record + remote_id: + type: string + example: payroll_run_1234 nullable: true - description: Whether the candidate is private - email_reachable: - type: boolean - example: true + description: >- + The remote ID of the employee payroll run in the context of the 3rd + Party + remote_data: + type: object + example: + raw_data: + additional_field: some value nullable: true - description: Whether the candidate is reachable by email + description: >- + The remote data of the employee payroll run in the context of the + 3rd Party remote_created_at: + format: date-time type: string example: '2024-10-01T12:00:00Z' - format: date-time nullable: true - description: The remote creation date of the candidate - remote_modified_at: + description: >- + The date when the employee payroll run was created in the 3rd party + system + created_at: + format: date-time type: string example: '2024-10-01T12:00:00Z' - format: date-time nullable: true - description: The remote modification date of the candidate - last_interaction_at: + description: The created date of the employee payroll run record + modified_at: + format: date-time type: string example: '2024-10-01T12:00:00Z' - format: date-time nullable: true - description: The last interaction date with the candidate - attachments: + description: The last modified date of the employee payroll run record + remote_was_deleted: + type: boolean + example: false + nullable: true + description: >- + Indicates if the employee payroll run was deleted in the remote + system + UnifiedHrisEmployeeOutput: + type: object + properties: + groups: + example: &ref_124 + - Group1 + - Group2 + nullable: true + description: The groups the employee belongs to type: array - items: &ref_130 - oneOf: - - type: string - - $ref: '#/components/schemas/UnifiedAtsAttachmentOutput' - example: &ref_131 + items: + type: string + locations: + example: &ref_125 - 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The attachments UUIDs of the candidate - applications: + description: UUIDs of the of the Location associated with the company type: array - items: &ref_132 - oneOf: - - type: string - - $ref: '#/components/schemas/UnifiedAtsApplicationOutput' - example: &ref_133 - - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + items: + type: string + employee_number: + type: string + example: EMP001 nullable: true - description: The applications UUIDs of the candidate - tags: - type: array - items: &ref_134 - oneOf: - - type: string - - $ref: '#/components/schemas/UnifiedAtsTagOutput' - example: &ref_135 - - tag_1 - - tag_2 + description: The employee number + company_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The tags of the candidate - urls: - example: &ref_136 - - url: mywebsite.com - url_type: WEBSITE + description: The UUID of the associated company + first_name: + type: string + example: John nullable: true - description: >- - The urls of the candidate, possible values for Url type are WEBSITE, - BLOG, LINKEDIN, GITHUB, or OTHER - type: array - items: - $ref: '#/components/schemas/Url' - phone_numbers: - example: &ref_137 - - phone_number: '+33660688899' - phone_type: WORK + description: The first name of the employee + last_name: + type: string + example: Doe nullable: true - description: The phone numbers of the candidate - type: array - items: - $ref: '#/components/schemas/Phone' - email_addresses: - example: &ref_138 - - email_address: joedoe@gmail.com - email_address_type: WORK + description: The last name of the employee + preferred_name: + type: string + example: Johnny nullable: true - description: The email addresses of the candidate + description: The preferred name of the employee + display_full_name: + type: string + example: John Doe + nullable: true + description: The full display name of the employee + username: + type: string + example: johndoe + nullable: true + description: The username of the employee + work_email: + type: string + example: john.doe@company.com + nullable: true + description: The work email of the employee + personal_email: + type: string + example: john.doe@personal.com + nullable: true + description: The personal email of the employee + mobile_phone_number: + type: string + example: '+1234567890' + nullable: true + description: The mobile phone number of the employee + employments: + example: &ref_126 + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The employments of the employee type: array items: - $ref: '#/components/schemas/Email' + type: string + ssn: + type: string + example: 123-45-6789 + nullable: true + description: The Social Security Number of the employee + gender: + type: string + example: MALE + enum: &ref_127 + - MALE + - FEMALE + - NON-BINARY + - OTHER + - PREFER_NOT_TO_DISCLOSE + nullable: true + description: The gender of the employee + ethnicity: + type: string + example: AMERICAN_INDIAN_OR_ALASKA_NATIVE + enum: &ref_128 + - AMERICAN_INDIAN_OR_ALASKA_NATIVE + - ASIAN_OR_INDIAN_SUBCONTINENT + - BLACK_OR_AFRICAN_AMERICAN + - HISPANIC_OR_LATINO + - NATIVE_HAWAIIAN_OR_OTHER_PACIFIC_ISLANDER + - TWO_OR_MORE_RACES + - WHITE + - PREFER_NOT_TO_DISCLOSE + nullable: true + description: The ethnicity of the employee + marital_status: + type: string + example: Married + enum: &ref_129 + - SINGLE + - MARRIED_FILING_JOINTLY + - MARRIED_FILING_SEPARATELY + - HEAD_OF_HOUSEHOLD + - QUALIFYING_WIDOW_OR_WIDOWER_WITH_DEPENDENT_CHILD + nullable: true + description: The marital status of the employee + date_of_birth: + format: date-time + type: string + example: '1990-01-01' + nullable: true + description: The date of birth of the employee + start_date: + format: date-time + type: string + example: '2020-01-01' + nullable: true + description: The start date of the employee + employment_status: + type: string + example: ACTIVE + enum: &ref_130 + - ACTIVE + - PENDING + - INACTIVE + nullable: true + description: The employment status of the employee + termination_date: + format: date-time + type: string + example: '2025-01-01' + nullable: true + description: The termination date of the employee + avatar_url: + type: string + example: https://example.com/avatar.jpg + nullable: true + description: The URL of the employee's avatar + manager_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: UUID of the manager (employee) of the employee field_mappings: type: object - example: &ref_139 - fav_dish: broccoli - fav_color: red - additionalProperties: true + example: &ref_131 + custom_field_1: value1 + custom_field_2: value2 nullable: true description: >- The custom field mappings of the object between the remote 3rd party @@ -12871,151 +13037,214 @@ components: type: string example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The UUID of the candidate + description: The UUID of the employee record remote_id: type: string - example: id_1 + example: employee_1234 nullable: true - description: The id of the candidate in the context of the 3rd Party + description: The remote ID of the employee in the context of the 3rd Party remote_data: type: object example: - fav_dish: broccoli - fav_color: red + raw_data: + additional_field: some value nullable: true - additionalProperties: true - description: The remote data of the candidate in the context of the 3rd Party + description: The remote data of the employee in the context of the 3rd Party + remote_created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The date when the employee was created in the 3rd party system created_at: format: date-time type: string example: '2024-10-01T12:00:00Z' nullable: true - description: The created date of the object + description: The created date of the employee record modified_at: format: date-time type: string example: '2024-10-01T12:00:00Z' nullable: true - description: The modified date of the object - UnifiedAtsCandidateInput: + description: The last modified date of the employee record + remote_was_deleted: + type: boolean + example: false + nullable: true + description: Indicates if the employee was deleted in the remote system + UnifiedHrisEmployeeInput: type: object properties: + groups: + example: *ref_124 + nullable: true + description: The groups the employee belongs to + type: array + items: + type: string + locations: + example: *ref_125 + nullable: true + description: UUIDs of the of the Location associated with the company + type: array + items: + type: string + employee_number: + type: string + example: EMP001 + nullable: true + description: The employee number + company_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated company first_name: type: string - example: Joe + example: John nullable: true - description: The first name of the candidate + description: The first name of the employee last_name: type: string example: Doe nullable: true - description: The last name of the candidate - company: + description: The last name of the employee + preferred_name: type: string - example: Acme + example: Johnny nullable: true - description: The company of the candidate - title: + description: The preferred name of the employee + display_full_name: type: string - example: Analyst + example: John Doe nullable: true - description: The title of the candidate - locations: + description: The full display name of the employee + username: type: string - example: New York + example: johndoe nullable: true - description: The locations of the candidate - is_private: - type: boolean - example: false + description: The username of the employee + work_email: + type: string + example: john.doe@company.com nullable: true - description: Whether the candidate is private - email_reachable: - type: boolean - example: true + description: The work email of the employee + personal_email: + type: string + example: john.doe@personal.com nullable: true - description: Whether the candidate is reachable by email - remote_created_at: + description: The personal email of the employee + mobile_phone_number: type: string - example: '2024-10-01T12:00:00Z' - format: date-time + example: '+1234567890' nullable: true - description: The remote creation date of the candidate - remote_modified_at: + description: The mobile phone number of the employee + employments: + example: *ref_126 + nullable: true + description: The employments of the employee + type: array + items: + type: string + ssn: type: string - example: '2024-10-01T12:00:00Z' - format: date-time + example: 123-45-6789 nullable: true - description: The remote modification date of the candidate - last_interaction_at: + description: The Social Security Number of the employee + gender: type: string - example: '2024-10-01T12:00:00Z' - format: date-time + example: MALE + enum: *ref_127 nullable: true - description: The last interaction date with the candidate - attachments: - type: array - items: *ref_130 - example: *ref_131 + description: The gender of the employee + ethnicity: + type: string + example: AMERICAN_INDIAN_OR_ALASKA_NATIVE + enum: *ref_128 nullable: true - description: The attachments UUIDs of the candidate - applications: - type: array - items: *ref_132 - example: *ref_133 + description: The ethnicity of the employee + marital_status: + type: string + example: Married + enum: *ref_129 nullable: true - description: The applications UUIDs of the candidate - tags: - type: array - items: *ref_134 - example: *ref_135 + description: The marital status of the employee + date_of_birth: + format: date-time + type: string + example: '1990-01-01' nullable: true - description: The tags of the candidate - urls: - example: *ref_136 + description: The date of birth of the employee + start_date: + format: date-time + type: string + example: '2020-01-01' nullable: true - description: >- - The urls of the candidate, possible values for Url type are WEBSITE, - BLOG, LINKEDIN, GITHUB, or OTHER - type: array - items: - $ref: '#/components/schemas/Url' - phone_numbers: - example: *ref_137 + description: The start date of the employee + employment_status: + type: string + example: ACTIVE + enum: *ref_130 nullable: true - description: The phone numbers of the candidate - type: array - items: - $ref: '#/components/schemas/Phone' - email_addresses: - example: *ref_138 + description: The employment status of the employee + termination_date: + format: date-time + type: string + example: '2025-01-01' nullable: true - description: The email addresses of the candidate - type: array - items: - $ref: '#/components/schemas/Email' + description: The termination date of the employee + avatar_url: + type: string + example: https://example.com/avatar.jpg + nullable: true + description: The URL of the employee's avatar + manager_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: UUID of the manager (employee) of the employee field_mappings: type: object - example: *ref_139 - additionalProperties: true + example: *ref_131 nullable: true description: >- The custom field mappings of the object between the remote 3rd party & Panora - UnifiedAtsDepartmentOutput: + UnifiedHrisEmployerbenefitOutput: type: object properties: + benefit_plan_type: + type: string + example: Health Insurance + enum: + - MEDICAL + - HEALTH_SAVINGS + - INSURANCE + - RETIREMENT + - OTHER + nullable: true + description: The type of the benefit plan name: type: string - example: Sales + example: Company Health Plan nullable: true - description: The name of the department + description: The name of the employer benefit + description: + type: string + example: Comprehensive health insurance coverage for employees + nullable: true + description: The description of the employer benefit + deduction_code: + type: string + example: HEALTH-001 + nullable: true + description: The deduction code for the employer benefit field_mappings: type: object example: - fav_dish: broccoli - fav_color: red - additionalProperties: true + custom_field_1: value1 + custom_field_2: value2 nullable: true description: >- The custom field mappings of the object between the remote 3rd party @@ -13024,103 +13253,301 @@ components: type: string example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The UUID of the department + description: The UUID of the employer benefit record remote_id: type: string - example: id_1 + example: benefit_1234 nullable: true - description: The remote ID of the department in the context of the 3rd Party + description: >- + The remote ID of the employer benefit in the context of the 3rd + Party remote_data: type: object example: - key1: value1 - key2: 42 - key3: true + raw_data: + additional_field: some value nullable: true - additionalProperties: true - description: The remote data of the department in the context of the 3rd Party + description: >- + The remote data of the employer benefit in the context of the 3rd + Party + remote_created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: >- + The date when the employer benefit was created in the 3rd party + system created_at: format: date-time type: string example: '2024-10-01T12:00:00Z' nullable: true - description: The created date of the object + description: The created date of the employer benefit record modified_at: format: date-time type: string - example: '2023-10-01T12:00:00Z' + example: '2024-10-01T12:00:00Z' nullable: true - description: The modified date of the object - UnifiedAtsInterviewOutput: + description: The last modified date of the employer benefit record + remote_was_deleted: + type: boolean + example: false + nullable: true + description: Indicates if the employer benefit was deleted in the remote system + UnifiedHrisEmploymentOutput: type: object properties: - status: - type: string - enum: &ref_140 - - SCHEDULED - - AWAITING_FEEDBACK - - COMPLETED - example: SCHEDULED - nullable: true - description: The status of the interview - application_id: + job_title: type: string - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + example: Software Engineer nullable: true - description: The UUID of the application - job_interview_stage_id: - type: string - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + description: The job title of the employment + pay_rate: + type: number + example: 100000 nullable: true - description: The UUID of the job interview stage - organized_by: + description: The pay rate of the employment + pay_period: type: string - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - nullable: true - description: The UUID of the organizer - interviewers: - example: &ref_141 - - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + example: MONTHLY + enum: + - HOUR + - DAY + - WEEK + - EVERY_TWO_WEEKS + - SEMIMONTHLY + - MONTH + - QUARTER + - EVERY_SIX_MONTHS + - YEAR + nullable: true + description: The pay period of the employment + pay_frequency: + type: string + example: WEEKLY + enum: + - WEEKLY + - BIWEEKLY + - MONTHLY + - QUARTERLY + - SEMIANNUALLY + - ANNUALLY + - THIRTEEN-MONTHLY + - PRO_RATA + - SEMIMONTHLY + nullable: true + description: The pay frequency of the employment + pay_currency: + type: string + example: USD + enum: + - AED + - AFN + - ALL + - AMD + - ANG + - AOA + - ARS + - AUD + - AWG + - AZN + - BAM + - BBD + - BDT + - BGN + - BHD + - BIF + - BMD + - BND + - BOB + - BRL + - BSD + - BTN + - BWP + - BYN + - BZD + - CAD + - CDF + - CHF + - CLP + - CNY + - COP + - CRC + - CUP + - CVE + - CZK + - DJF + - DKK + - DOP + - DZD + - EGP + - ERN + - ETB + - EUR + - FJD + - FKP + - FOK + - GBP + - GEL + - GGP + - GHS + - GIP + - GMD + - GNF + - GTQ + - GYD + - HKD + - HNL + - HRK + - HTG + - HUF + - IDR + - ILS + - IMP + - INR + - IQD + - IRR + - ISK + - JEP + - JMD + - JOD + - JPY + - KES + - KGS + - KHR + - KID + - KMF + - KRW + - KWD + - KYD + - KZT + - LAK + - LBP + - LKR + - LRD + - LSL + - LYD + - MAD + - MDL + - MGA + - MKD + - MMK + - MNT + - MOP + - MRU + - MUR + - MVR + - MWK + - MXN + - MYR + - MZN + - NAD + - NGN + - NIO + - NOK + - NPR + - NZD + - OMR + - PAB + - PEN + - PGK + - PHP + - PKR + - PLN + - PYG + - QAR + - RON + - RSD + - RUB + - RWF + - SAR + - SBD + - SCR + - SDG + - SEK + - SGD + - SHP + - SLE + - SLL + - SOS + - SRD + - SSP + - STN + - SYP + - SZL + - THB + - TJS + - TMT + - TND + - TOP + - TRY + - TTD + - TVD + - TWD + - TZS + - UAH + - UGX + - USD + - UYU + - UZS + - VES + - VND + - VUV + - WST + - XAF + - XCD + - XDR + - XOF + - XPF + - YER + - ZAR + - ZMW + - ZWL nullable: true - description: The UUIDs of the interviewers - type: array - items: - type: string - location: + description: The currency of the pay + flsa_status: type: string - example: San Francisco + example: EXEMPT + enum: + - EXEMPT + - SALARIED_NONEXEMPT + - NONEXEMPT + - OWNER nullable: true - description: The location of the interview - start_at: + description: The FLSA status of the employment + effective_date: format: date-time type: string - example: '2024-10-01T12:00:00Z' + example: '2023-01-01' nullable: true - description: The start date and time of the interview - end_at: - format: date-time + description: The effective date of the employment + employment_type: type: string - example: '2024-10-01T12:00:00Z' + example: FULL_TIME + enum: + - FULL_TIME + - PART_TIME + - INTERN + - CONTRACTOR + - FREELANCE nullable: true - description: The end date and time of the interview - remote_created_at: - format: date-time + description: The type of employment + pay_group_id: type: string - example: '2024-10-01T12:00:00Z' + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The remote creation date of the interview - remote_updated_at: - format: date-time + description: The UUID of the associated pay group + employee_id: type: string - example: '2024-10-01T12:00:00Z' + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The remote modification date of the interview + description: The UUID of the associated employee field_mappings: type: object - example: &ref_142 - fav_dish: broccoli - fav_color: red - additionalProperties: true + example: + custom_field_1: value1 + custom_field_2: value2 nullable: true description: >- The custom field mappings of the object between the remote 3rd party @@ -13129,124 +13556,71 @@ components: type: string example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The UUID of the interview + description: The UUID of the employment record remote_id: type: string - example: id_1 + example: employment_1234 nullable: true - description: The remote ID of the interview in the context of the 3rd Party + description: The remote ID of the employment in the context of the 3rd Party remote_data: type: object example: - fav_dish: broccoli - fav_color: red + raw_data: + additional_field: some value nullable: true - additionalProperties: true - description: The remote data of the interview in the context of the 3rd Party - created_at: + description: The remote data of the employment in the context of the 3rd Party + remote_created_at: format: date-time type: string example: '2024-10-01T12:00:00Z' nullable: true - description: The created date of the object - modified_at: - format: date-time - type: string - example: '2024-10-01T12:00:00Z' - nullable: true - description: The modified date of the object - UnifiedAtsInterviewInput: - type: object - properties: - status: - type: string - enum: *ref_140 - example: SCHEDULED - nullable: true - description: The status of the interview - application_id: - type: string - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - nullable: true - description: The UUID of the application - job_interview_stage_id: - type: string - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - nullable: true - description: The UUID of the job interview stage - organized_by: - type: string - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - nullable: true - description: The UUID of the organizer - interviewers: - example: *ref_141 - nullable: true - description: The UUIDs of the interviewers - type: array - items: - type: string - location: - type: string - example: San Francisco - nullable: true - description: The location of the interview - start_at: - format: date-time - type: string - example: '2024-10-01T12:00:00Z' - nullable: true - description: The start date and time of the interview - end_at: - format: date-time - type: string - example: '2024-10-01T12:00:00Z' - nullable: true - description: The end date and time of the interview - remote_created_at: + description: The date when the employment was created in the 3rd party system + created_at: format: date-time type: string example: '2024-10-01T12:00:00Z' nullable: true - description: The remote creation date of the interview - remote_updated_at: + description: The created date of the employment record + modified_at: format: date-time type: string example: '2024-10-01T12:00:00Z' nullable: true - description: The remote modification date of the interview - field_mappings: - type: object - example: *ref_142 - additionalProperties: true + description: The last modified date of the employment record + remote_was_deleted: + type: boolean + example: false nullable: true - description: >- - The custom field mappings of the object between the remote 3rd party - & Panora - UnifiedAtsJobinterviewstageOutput: + description: Indicates if the employment was deleted in the remote system + UnifiedHrisGroupOutput: type: object properties: - name: + parent_group: type: string - example: Second Call + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The name of the job interview stage - stage_order: - type: number - example: 1 + description: The UUID of the parent group + name: + type: string + example: Engineering Team nullable: true - description: The order of the stage - job_id: + description: The name of the group + type: type: string - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + example: DEPARTMENT + enum: + - TEAM + - DEPARTMENT + - COST_CENTER + - BUSINESS_UNIT + - GROUP nullable: true - description: The UUID of the job + description: The type of the group field_mappings: type: object example: - fav_dish: broccoli - fav_color: red - additionalProperties: true + custom_field_1: value1 + custom_field_2: value2 nullable: true description: >- The custom field mappings of the object between the remote 3rd party @@ -13255,129 +13629,108 @@ components: type: string example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The UUID of the job interview stage + description: The UUID of the group record remote_id: type: string - example: id_1 + example: group_1234 nullable: true - description: >- - The remote ID of the job interview stage in the context of the 3rd - Party + description: The remote ID of the group in the context of the 3rd Party remote_data: type: object example: - fav_dish: broccoli - fav_color: red + raw_data: + additional_field: some value nullable: true - additionalProperties: true - description: >- - The remote data of the job interview stage in the context of the 3rd - Party + description: The remote data of the group in the context of the 3rd Party + remote_created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The date when the group was created in the 3rd party system created_at: format: date-time type: string example: '2024-10-01T12:00:00Z' nullable: true - description: The created date of the object + description: The created date of the group record modified_at: format: date-time type: string example: '2024-10-01T12:00:00Z' nullable: true - description: The modified date of the object - UnifiedAtsJobOutput: + description: The last modified date of the group record + remote_was_deleted: + type: boolean + example: false + nullable: true + description: Indicates if the group was deleted in the remote system + UnifiedHrisLocationOutput: type: object properties: name: type: string - example: Financial Analyst + example: Headquarters nullable: true - description: The name of the job - description: + description: The name of the location + phone_number: type: string - example: Extract financial data and write detailed investment thesis + example: '+1234567890' nullable: true - description: The description of the job - code: + description: The phone number of the location + street_1: type: string - example: JOB123 + example: 123 Main St nullable: true - description: The code of the job - status: + description: The first line of the street address + street_2: type: string - enum: - - OPEN - - CLOSED - - DRAFT - - ARCHIVED - - PENDING - example: OPEN + example: Suite 456 nullable: true - description: The status of the job - type: + description: The second line of the street address + city: type: string - example: POSTING - enum: - - POSTING - - REQUISITION - - PROFILE - nullable: true - description: The type of the job - confidential: - type: boolean - example: true + example: San Francisco nullable: true - description: Whether the job is confidential - departments: - example: - - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + description: The city of the location + state: + type: string + example: CA nullable: true - description: The departments UUIDs associated with the job - type: array - items: - type: string - offices: - example: - - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + description: The state or region of the location + zip_code: + type: string + example: '94105' nullable: true - description: The offices UUIDs associated with the job - type: array - items: - type: string - managers: - example: - - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + description: The zip or postal code of the location + country: + type: string + example: USA nullable: true - description: The managers UUIDs associated with the job - type: array - items: - type: string - recruiters: - example: - - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + description: The country of the location + location_type: + type: string + example: WORK + enum: + - WORK + - HOME nullable: true - description: The recruiters UUIDs associated with the job - type: array - items: - type: string - remote_created_at: + description: The type of the location + company_id: type: string - example: '2024-10-01T12:00:00Z' - format: date-time + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The remote creation date of the job - remote_updated_at: + description: The UUID of the company associated with the location + employee_id: type: string - example: '2024-10-01T12:00:00Z' - format: date-time + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The remote modification date of the job + description: The UUID of the employee associated with the location field_mappings: type: object example: - fav_dish: broccoli - fav_color: red - additionalProperties: true + custom_field_1: value1 + custom_field_2: value2 nullable: true description: >- The custom field mappings of the object between the remote 3rd party @@ -13386,265 +13739,411 @@ components: type: string example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The UUID of the job + description: The UUID of the location record remote_id: type: string - example: id_1 + example: location_1234 nullable: true - description: The remote ID of the job in the context of the 3rd Party + description: The remote ID of the location in the context of the 3rd Party remote_data: type: object example: - key1: value1 - key2: 42 - key3: true + raw_data: + additional_field: some value nullable: true - additionalProperties: true - description: The remote data of the job in the context of the 3rd Party + description: The remote data of the location in the context of the 3rd Party + remote_created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The date when the location was created in the 3rd party system created_at: format: date-time type: string example: '2024-10-01T12:00:00Z' nullable: true - description: The created date of the object + description: The created date of the location record modified_at: format: date-time type: string - example: '2023-10-01T12:00:00Z' + example: '2024-10-01T12:00:00Z' nullable: true - description: The modified date of the object - UnifiedAtsOfferOutput: + description: The last modified date of the location record + remote_was_deleted: + type: boolean + example: false + nullable: true + description: Indicates if the location was deleted in the remote system + UnifiedHrisPaygroupOutput: type: object properties: - created_by: + pay_group_name: + type: string + example: Monthly Salaried + nullable: true + description: The name of the pay group + field_mappings: + type: object + example: + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: type: string example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - description: The UUID of the creator nullable: true - remote_created_at: - format: date-time + description: The UUID of the pay group record + remote_id: type: string - example: '2024-10-01T12:00:00Z' - description: The remote creation date of the offer + example: paygroup_1234 nullable: true - closed_at: + description: The remote ID of the pay group in the context of the 3rd Party + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: The remote data of the pay group in the context of the 3rd Party + remote_created_at: format: date-time type: string example: '2024-10-01T12:00:00Z' - description: The closing date of the offer nullable: true - sent_at: + description: The date when the pay group was created in the 3rd party system + created_at: format: date-time type: string example: '2024-10-01T12:00:00Z' - description: The sending date of the offer nullable: true - start_date: + description: The created date of the pay group record + modified_at: format: date-time type: string example: '2024-10-01T12:00:00Z' - description: The start date of the offer nullable: true - status: - type: string - example: DRAFT - enum: - - DRAFT - - APPROVAL_SENT + description: The last modified date of the pay group record + remote_was_deleted: + type: boolean + example: false + nullable: true + description: Indicates if the pay group was deleted in the remote system + UnifiedHrisPayrollrunOutput: + type: object + properties: + run_state: + type: string + example: PAID + enum: + - PAID + - DRAFT - APPROVED - - SENT - - SENT_MANUALLY - - OPENED - - DENIED - - SIGNED - - DEPRECATED - description: The status of the offer + - FAILED + - CLOSE nullable: true - application_id: + description: The state of the payroll run + run_type: type: string - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - description: The UUID of the application + example: REGULAR + enum: + - REGULAR + - OFF_CYCLE + - CORRECTION + - TERMINATION + - SIGN_ON_BONUS + nullable: true + description: The type of the payroll run + start_date: + format: date-time + type: string + example: '2024-01-01T00:00:00Z' + nullable: true + description: The start date of the payroll run + end_date: + format: date-time + type: string + example: '2024-01-15T23:59:59Z' + nullable: true + description: The end date of the payroll run + check_date: + format: date-time + type: string + example: '2024-01-20T00:00:00Z' nullable: true + description: The check date of the payroll run field_mappings: type: object example: - fav_dish: broccoli - fav_color: red + custom_field_1: value1 + custom_field_2: value2 + nullable: true description: >- The custom field mappings of the object between the remote 3rd party & Panora - nullable: true - additionalProperties: true id: type: string example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - description: The UUID of the offer nullable: true + description: The UUID of the payroll run record remote_id: type: string - example: id_1 - description: The remote ID of the offer in the context of the 3rd Party + example: payroll_run_1234 nullable: true + description: The remote ID of the payroll run in the context of the 3rd Party remote_data: type: object example: - fav_dish: broccoli - fav_color: red - description: The remote data of the offer in the context of the 3rd Party + raw_data: + additional_field: some value nullable: true - additionalProperties: true + description: The remote data of the payroll run in the context of the 3rd Party + remote_created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The date when the payroll run was created in the 3rd party system created_at: - type: object + format: date-time + type: string example: '2024-10-01T12:00:00Z' - description: The created date of the object nullable: true + description: The created date of the payroll run record modified_at: - type: object + format: date-time + type: string example: '2024-10-01T12:00:00Z' - description: The modified date of the object nullable: true - UnifiedAtsOfficeOutput: + description: The last modified date of the payroll run record + remote_was_deleted: + type: boolean + example: false + nullable: true + description: Indicates if the payroll run was deleted in the remote system + employee_payroll_runs: + example: + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: >- + The UUIDs of the employee payroll runs associated with this payroll + run + type: array + items: + type: string + UnifiedHrisTimeoffOutput: type: object properties: - name: + employee: type: string - example: Condo Office 5th + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The name of the office - location: + description: The UUID of the employee taking time off + approver: type: string - example: New York + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The location of the office - field_mappings: - type: object - example: - fav_dish: broccoli - fav_color: red - additionalProperties: true + description: The UUID of the approver for the time off request + status: + type: string + example: REQUESTED + enum: &ref_132 + - REQUESTED + - APPROVED + - DECLINED + - CANCELLED + - DELETED nullable: true - description: >- - The custom field mappings of the object between the remote 3rd party - & Panora - id: + description: The status of the time off request + employee_note: type: string - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - description: The UUID of the office - remote_id: + example: Annual vacation + nullable: true + description: A note from the employee about the time off request + units: type: string - example: id_1 + example: DAYS + enum: &ref_133 + - HOURS + - DAYS nullable: true - description: The remote ID of the office in the context of the 3rd Party - remote_data: - type: object - example: - fav_dish: broccoli - fav_color: red + description: The units used for the time off (e.g., Days, Hours) + amount: + type: number + example: 5 nullable: true - additionalProperties: true - description: The remote data of the office in the context of the 3rd Party - created_at: - format: date-time + description: The amount of time off requested + request_type: type: string - example: '2024-10-01T12:00:00Z' + example: VACATION + enum: &ref_134 + - VACATION + - SICK + - PERSONAL + - JURY_DUTY + - VOLUNTEER + - BEREAVEMENT nullable: true - description: The created date of the object - modified_at: + description: The type of time off request + start_time: format: date-time type: string - example: '2024-10-01T12:00:00Z' + example: '2024-07-01T09:00:00Z' nullable: true - description: The modified date of the object - UnifiedAtsRejectreasonOutput: - type: object - properties: - name: + description: The start time of the time off + end_time: + format: date-time type: string - example: Candidate inexperienced + example: '2024-07-05T17:00:00Z' nullable: true - description: The name of the reject reason + description: The end time of the time off field_mappings: type: object - example: - fav_dish: broccoli - fav_color: red - additionalProperties: true + example: &ref_135 + custom_field_1: value1 + custom_field_2: value2 nullable: true description: >- The custom field mappings of the object between the remote 3rd party & Panora id: type: string - nullable: true - description: The UUID of the reject reason example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the time off record remote_id: type: string + example: timeoff_1234 nullable: true - description: The remote ID of the reject reason in the context of the 3rd Party - example: id_1 + description: The remote ID of the time off in the context of the 3rd Party remote_data: type: object example: - fav_dish: broccoli - fav_color: red + raw_data: + additional_field: some value nullable: true - additionalProperties: true - description: The remote data of the reject reason in the context of the 3rd Party + description: The remote data of the time off in the context of the 3rd Party + remote_created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date when the time off was created in the 3rd party system created_at: format: date-time type: string - example: '2024-10-01T12:00:00Z' + example: '2024-06-15T12:00:00Z' nullable: true - description: The created date of the object + description: The created date of the time off record modified_at: format: date-time type: string - example: '2024-10-01T12:00:00Z' + example: '2024-06-15T12:00:00Z' nullable: true - description: The modified date of the object - UnifiedAtsScorecardOutput: + description: The last modified date of the time off record + remote_was_deleted: + type: boolean + example: false + nullable: true + description: Indicates if the time off was deleted in the remote system + UnifiedHrisTimeoffInput: type: object properties: - overall_recommendation: + employee: type: string - enum: - - DEFINITELY_NO - - 'NO' - - 'YES' - - STRONG_YES - - NO_DECISION - example: 'YES' + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The overall recommendation - application_id: + description: The UUID of the employee taking time off + approver: type: string example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The UUID of the application - interview_id: + description: The UUID of the approver for the time off request + status: type: string - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + example: REQUESTED + enum: *ref_132 nullable: true - description: The UUID of the interview - remote_created_at: + description: The status of the time off request + employee_note: type: string - example: '2024-10-01T12:00:00Z' - format: date-time + example: Annual vacation nullable: true - description: The remote creation date of the scorecard - submitted_at: + description: A note from the employee about the time off request + units: type: string - example: '2024-10-01T12:00:00Z' + example: DAYS + enum: *ref_133 + nullable: true + description: The units used for the time off (e.g., Days, Hours) + amount: + type: number + example: 5 + nullable: true + description: The amount of time off requested + request_type: + type: string + example: VACATION + enum: *ref_134 + nullable: true + description: The type of time off request + start_time: format: date-time + type: string + example: '2024-07-01T09:00:00Z' nullable: true - description: The submission date of the scorecard + description: The start time of the time off + end_time: + format: date-time + type: string + example: '2024-07-05T17:00:00Z' + nullable: true + description: The end time of the time off + field_mappings: + type: object + example: *ref_135 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + UnifiedHrisTimeoffbalanceOutput: + type: object + properties: + balance: + type: number + example: 80 + nullable: true + description: The current balance of time off + employee_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated employee + used: + type: number + example: 40 + nullable: true + description: The amount of time off used + policy_type: + type: string + example: VACATION + enum: + - VACATION + - SICK + - PERSONAL + - JURY_DUTY + - VOLUNTEER + - BEREAVEMENT + nullable: true + description: The type of time off policy field_mappings: type: object example: - fav_dish: broccoli - fav_color: red - additionalProperties: true + custom_field_1: value1 + custom_field_2: value2 nullable: true description: >- The custom field mappings of the object between the remote 3rd party @@ -13652,51 +14151,80 @@ components: id: type: string example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - description: The UUID of the scorecard + nullable: true + description: The UUID of the time off balance record remote_id: type: string - example: id_1 + example: timeoff_balance_1234 nullable: true - description: The remote ID of the scorecard in the context of the 3rd Party + description: >- + The remote ID of the time off balance in the context of the 3rd + Party remote_data: type: object example: - fav_dish: broccoli - fav_color: red + raw_data: + additional_field: some value nullable: true - additionalProperties: true - description: The remote data of the scorecard in the context of the 3rd Party + description: >- + The remote data of the time off balance in the context of the 3rd + Party + remote_created_at: + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: >- + The date when the time off balance was created in the 3rd party + system created_at: - format: date-time type: string - example: '2024-10-01T12:00:00Z' + example: '2024-06-15T12:00:00Z' nullable: true - description: The created date of the object + description: The created date of the time off balance record modified_at: - format: date-time type: string - example: '2024-10-01T12:00:00Z' + example: '2024-06-15T12:00:00Z' nullable: true - description: The modified date of the object - UnifiedAtsTagOutput: + description: The last modified date of the time off balance record + remote_was_deleted: + type: boolean + example: false + nullable: true + description: Indicates if the time off balance was deleted in the remote system + UnifiedHrisTimesheetEntryOutput: type: object properties: - name: + hours_worked: + type: number + example: 40 + nullable: true + description: The number of hours worked + start_time: + format: date-time type: string - example: Important + example: '2024-10-01T08:00:00Z' nullable: true - description: The name of the tag - id_ats_candidate: + description: The start time of the timesheet entry + end_time: + format: date-time + type: string + example: '2024-10-01T16:00:00Z' + nullable: true + description: The end time of the timesheet entry + employee_id: type: string example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The UUID of the candidate + description: The UUID of the associated employee + remote_was_deleted: + type: boolean + example: false + description: Indicates if the timesheet entry was deleted in the remote system field_mappings: type: object - example: - fav_dish: broccoli - fav_color: red - additionalProperties: true + example: &ref_136 + custom_field_1: value1 + custom_field_2: value2 nullable: true description: >- The custom field mappings of the object between the remote 3rd party @@ -13705,12 +14233,183 @@ components: type: string example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - description: The UUID of the tag + description: The UUID of the timesheet entry record remote_id: type: string example: id_1 nullable: true - description: The remote ID of the tag in the context of the 3rd Party + description: The remote ID of the timesheet entry + remote_created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The date when the timesheet entry was created in the remote system + created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + description: The created date of the timesheet entry + modified_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + description: The last modified date of the timesheet entry + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: >- + The remote data of the timesheet entry in the context of the 3rd + Party + UnifiedHrisTimesheetEntryInput: + type: object + properties: + hours_worked: + type: number + example: 40 + nullable: true + description: The number of hours worked + start_time: + format: date-time + type: string + example: '2024-10-01T08:00:00Z' + nullable: true + description: The start time of the timesheet entry + end_time: + format: date-time + type: string + example: '2024-10-01T16:00:00Z' + nullable: true + description: The end time of the timesheet entry + employee_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated employee + remote_was_deleted: + type: boolean + example: false + description: Indicates if the timesheet entry was deleted in the remote system + field_mappings: + type: object + example: *ref_136 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + UnifiedMarketingautomationActionOutput: + type: object + properties: {} + UnifiedMarketingautomationActionInput: + type: object + properties: {} + UnifiedMarketingautomationAutomationOutput: + type: object + properties: {} + UnifiedMarketingautomationAutomationInput: + type: object + properties: {} + UnifiedMarketingautomationCampaignOutput: + type: object + properties: {} + UnifiedMarketingautomationCampaignInput: + type: object + properties: {} + UnifiedMarketingautomationContactOutput: + type: object + properties: {} + UnifiedMarketingautomationContactInput: + type: object + properties: {} + UnifiedMarketingautomationEmailOutput: + type: object + properties: {} + UnifiedMarketingautomationEventOutput: + type: object + properties: {} + UnifiedMarketingautomationListOutput: + type: object + properties: {} + UnifiedMarketingautomationListInput: + type: object + properties: {} + UnifiedMarketingautomationMessageOutput: + type: object + properties: {} + UnifiedMarketingautomationTemplateOutput: + type: object + properties: {} + UnifiedMarketingautomationTemplateInput: + type: object + properties: {} + UnifiedMarketingautomationUserOutput: + type: object + properties: {} + UnifiedAtsActivityOutput: + type: object + properties: + activity_type: + type: string + enum: &ref_137 + - NOTE + - EMAIL + - OTHER + example: NOTE + nullable: true + description: The type of activity + subject: + type: string + example: Email subject + nullable: true + description: The subject of the activity + body: + type: string + example: Dear Diana, I love you + nullable: true + description: The body of the activity + visibility: + type: string + enum: &ref_138 + - ADMIN_ONLY + - PUBLIC + - PRIVATE + example: PUBLIC + nullable: true + description: The visibility of the activity + candidate_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the candidate + remote_created_at: + type: string + format: date-time + example: '2024-10-01T12:00:00Z' + nullable: true + description: The remote creation date of the activity + field_mappings: + type: object + example: &ref_139 + fav_dish: broccoli + fav_color: red + additionalProperties: true + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the activity + remote_id: + type: string + example: id_1 + nullable: true + description: The remote ID of the activity in the context of the 3rd Party remote_data: type: object example: @@ -13718,287 +14417,6389 @@ components: fav_color: red nullable: true additionalProperties: true - description: The remote data of the tag in the context of the 3rd Party + description: The remote data of the activity in the context of the 3rd Party created_at: format: date-time type: string - nullable: true example: '2024-10-01T12:00:00Z' - description: The creation date of the tag + nullable: true + description: The created date of the object modified_at: format: date-time type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The modified date of the object + UnifiedAtsActivityInput: + type: object + properties: + activity_type: + type: string + enum: *ref_137 + example: NOTE + nullable: true + description: The type of activity + subject: + type: string + example: Email subject + nullable: true + description: The subject of the activity + body: + type: string + example: Dear Diana, I love you + nullable: true + description: The body of the activity + visibility: + type: string + enum: *ref_138 + example: PUBLIC + nullable: true + description: The visibility of the activity + candidate_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true + description: The UUID of the candidate + remote_created_at: + type: string + format: date-time example: '2024-10-01T12:00:00Z' - description: The modification date of the tag - UnifiedAtsUserOutput: + nullable: true + description: The remote creation date of the activity + field_mappings: + type: object + example: *ref_139 + additionalProperties: true + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + UnifiedAtsApplicationOutput: type: object properties: - first_name: + applied_at: + format: date-time type: string - example: John - description: The first name of the user nullable: true - last_name: + description: The application date + example: '2024-10-01T12:00:00Z' + rejected_at: + format: date-time type: string - example: Doe - description: The last name of the user nullable: true - email: + description: The rejection date + example: '2024-10-01T12:00:00Z' + offers: + nullable: true + description: The offers UUIDs for the application + example: &ref_140 + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + - 12345678-1234-1234-1234-123456789012 + type: array + items: + type: string + source: + type: string + nullable: true + description: The source of the application + example: Source Name + credited_to: + type: string + nullable: true + description: The UUID of the person credited for the application + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + current_stage: + type: string + nullable: true + description: The UUID of the current stage of the application + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + reject_reason: + type: string + nullable: true + description: The rejection reason for the application + example: Candidate not experienced enough + candidate_id: + type: string + nullable: true + description: The UUID of the candidate + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + job_id: + type: string + description: The UUID of the job + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + field_mappings: + type: object + example: &ref_141 + fav_dish: broccoli + fav_color: red + additionalProperties: true + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + nullable: true + description: The UUID of the application + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + remote_id: + type: string + nullable: true + description: The remote ID of the application in the context of the 3rd Party + example: id_1 + remote_data: + type: object + example: + fav_dish: broccoli + fav_color: red + nullable: true + additionalProperties: true + description: The remote data of the application in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The created date of the object + modified_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The modified date of the object + remote_created_at: + format: date-time + type: string + nullable: true + description: The remote created date of the object + remote_modified_at: + format: date-time + type: string + nullable: true + description: The remote modified date of the object + UnifiedAtsApplicationInput: + type: object + properties: + applied_at: + format: date-time + type: string + nullable: true + description: The application date + example: '2024-10-01T12:00:00Z' + rejected_at: + format: date-time + type: string + nullable: true + description: The rejection date + example: '2024-10-01T12:00:00Z' + offers: + nullable: true + description: The offers UUIDs for the application + example: *ref_140 + type: array + items: + type: string + source: + type: string + nullable: true + description: The source of the application + example: Source Name + credited_to: + type: string + nullable: true + description: The UUID of the person credited for the application + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + current_stage: + type: string + nullable: true + description: The UUID of the current stage of the application + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + reject_reason: + type: string + nullable: true + description: The rejection reason for the application + example: Candidate not experienced enough + candidate_id: + type: string + nullable: true + description: The UUID of the candidate + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + job_id: + type: string + description: The UUID of the job + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + field_mappings: + type: object + example: *ref_141 + additionalProperties: true + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + UnifiedAtsAttachmentOutput: + type: object + properties: + file_url: + type: string + example: https://example.com/file.pdf + nullable: true + description: The URL of the file + file_name: + type: string + example: file.pdf + nullable: true + description: The name of the file + attachment_type: + type: string + example: RESUME + enum: &ref_142 + - RESUME + - COVER_LETTER + - OFFER_LETTER + - OTHER + nullable: true + description: The type of the file + remote_created_at: + type: string + example: '2024-10-01T12:00:00Z' + format: date-time + nullable: true + description: The remote creation date of the attachment + remote_modified_at: + type: string + example: '2024-10-01T12:00:00Z' + format: date-time + nullable: true + description: The remote modification date of the attachment + candidate_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the candidate + field_mappings: + type: object + example: &ref_143 + fav_dish: broccoli + fav_color: red + additionalProperties: true + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the attachment + remote_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The remote ID of the attachment + remote_data: + type: object + example: + fav_dish: broccoli + fav_color: red + nullable: true + additionalProperties: true + description: The remote data of the attachment in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The created date of the object + modified_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The modified date of the object + UnifiedAtsAttachmentInput: + type: object + properties: + file_url: + type: string + example: https://example.com/file.pdf + nullable: true + description: The URL of the file + file_name: + type: string + example: file.pdf + nullable: true + description: The name of the file + attachment_type: + type: string + example: RESUME + enum: *ref_142 + nullable: true + description: The type of the file + remote_created_at: + type: string + example: '2024-10-01T12:00:00Z' + format: date-time + nullable: true + description: The remote creation date of the attachment + remote_modified_at: + type: string + example: '2024-10-01T12:00:00Z' + format: date-time + nullable: true + description: The remote modification date of the attachment + candidate_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the candidate + field_mappings: + type: object + example: *ref_143 + additionalProperties: true + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + Url: + type: object + properties: + url: + type: string + nullable: true + description: The url. + url_type: + type: string + nullable: true + description: The url type. It takes [WEBSITE | BLOG | LINKEDIN | GITHUB | OTHER] + required: + - url + - url_type + UnifiedAtsCandidateOutput: + type: object + properties: + first_name: + type: string + example: Joe + nullable: true + description: The first name of the candidate + last_name: + type: string + example: Doe + nullable: true + description: The last name of the candidate + company: + type: string + example: Acme + nullable: true + description: The company of the candidate + title: + type: string + example: Analyst + nullable: true + description: The title of the candidate + locations: + type: string + example: New York + nullable: true + description: The locations of the candidate + is_private: + type: boolean + example: false + nullable: true + description: Whether the candidate is private + email_reachable: + type: boolean + example: true + nullable: true + description: Whether the candidate is reachable by email + remote_created_at: + type: string + example: '2024-10-01T12:00:00Z' + format: date-time + nullable: true + description: The remote creation date of the candidate + remote_modified_at: + type: string + example: '2024-10-01T12:00:00Z' + format: date-time + nullable: true + description: The remote modification date of the candidate + last_interaction_at: + type: string + example: '2024-10-01T12:00:00Z' + format: date-time + nullable: true + description: The last interaction date with the candidate + attachments: + type: array + items: &ref_144 + oneOf: + - type: string + - $ref: '#/components/schemas/UnifiedAtsAttachmentOutput' + example: &ref_145 + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The attachments UUIDs of the candidate + applications: + type: array + items: &ref_146 + oneOf: + - type: string + - $ref: '#/components/schemas/UnifiedAtsApplicationOutput' + example: &ref_147 + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The applications UUIDs of the candidate + tags: + type: array + items: &ref_148 + oneOf: + - type: string + - $ref: '#/components/schemas/UnifiedAtsTagOutput' + example: &ref_149 + - tag_1 + - tag_2 + nullable: true + description: The tags of the candidate + urls: + example: &ref_150 + - url: mywebsite.com + url_type: WEBSITE + nullable: true + description: >- + The urls of the candidate, possible values for Url type are WEBSITE, + BLOG, LINKEDIN, GITHUB, or OTHER + type: array + items: + $ref: '#/components/schemas/Url' + phone_numbers: + example: &ref_151 + - phone_number: '+33660688899' + phone_type: WORK + nullable: true + description: The phone numbers of the candidate + type: array + items: + $ref: '#/components/schemas/Phone' + email_addresses: + example: &ref_152 + - email_address: joedoe@gmail.com + email_address_type: WORK + nullable: true + description: The email addresses of the candidate + type: array + items: + $ref: '#/components/schemas/Email' + field_mappings: + type: object + example: &ref_153 + fav_dish: broccoli + fav_color: red + additionalProperties: true + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the candidate + remote_id: + type: string + example: id_1 + nullable: true + description: The id of the candidate in the context of the 3rd Party + remote_data: + type: object + example: + fav_dish: broccoli + fav_color: red + nullable: true + additionalProperties: true + description: The remote data of the candidate in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The created date of the object + modified_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The modified date of the object + UnifiedAtsCandidateInput: + type: object + properties: + first_name: + type: string + example: Joe + nullable: true + description: The first name of the candidate + last_name: + type: string + example: Doe + nullable: true + description: The last name of the candidate + company: + type: string + example: Acme + nullable: true + description: The company of the candidate + title: + type: string + example: Analyst + nullable: true + description: The title of the candidate + locations: + type: string + example: New York + nullable: true + description: The locations of the candidate + is_private: + type: boolean + example: false + nullable: true + description: Whether the candidate is private + email_reachable: + type: boolean + example: true + nullable: true + description: Whether the candidate is reachable by email + remote_created_at: + type: string + example: '2024-10-01T12:00:00Z' + format: date-time + nullable: true + description: The remote creation date of the candidate + remote_modified_at: + type: string + example: '2024-10-01T12:00:00Z' + format: date-time + nullable: true + description: The remote modification date of the candidate + last_interaction_at: + type: string + example: '2024-10-01T12:00:00Z' + format: date-time + nullable: true + description: The last interaction date with the candidate + attachments: + type: array + items: *ref_144 + example: *ref_145 + nullable: true + description: The attachments UUIDs of the candidate + applications: + type: array + items: *ref_146 + example: *ref_147 + nullable: true + description: The applications UUIDs of the candidate + tags: + type: array + items: *ref_148 + example: *ref_149 + nullable: true + description: The tags of the candidate + urls: + example: *ref_150 + nullable: true + description: >- + The urls of the candidate, possible values for Url type are WEBSITE, + BLOG, LINKEDIN, GITHUB, or OTHER + type: array + items: + $ref: '#/components/schemas/Url' + phone_numbers: + example: *ref_151 + nullable: true + description: The phone numbers of the candidate + type: array + items: + $ref: '#/components/schemas/Phone' + email_addresses: + example: *ref_152 + nullable: true + description: The email addresses of the candidate + type: array + items: + $ref: '#/components/schemas/Email' + field_mappings: + type: object + example: *ref_153 + additionalProperties: true + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + UnifiedAtsDepartmentOutput: + type: object + properties: + name: + type: string + example: Sales + nullable: true + description: The name of the department + field_mappings: + type: object + example: + fav_dish: broccoli + fav_color: red + additionalProperties: true + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the department + remote_id: + type: string + example: id_1 + nullable: true + description: The remote ID of the department in the context of the 3rd Party + remote_data: + type: object + example: + key1: value1 + key2: 42 + key3: true + nullable: true + additionalProperties: true + description: The remote data of the department in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The created date of the object + modified_at: + format: date-time + type: string + example: '2023-10-01T12:00:00Z' + nullable: true + description: The modified date of the object + UnifiedAtsInterviewOutput: + type: object + properties: + status: + type: string + enum: &ref_154 + - SCHEDULED + - AWAITING_FEEDBACK + - COMPLETED + example: SCHEDULED + nullable: true + description: The status of the interview + application_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the application + job_interview_stage_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the job interview stage + organized_by: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the organizer + interviewers: + example: &ref_155 + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUIDs of the interviewers + type: array + items: + type: string + location: + type: string + example: San Francisco + nullable: true + description: The location of the interview + start_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The start date and time of the interview + end_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The end date and time of the interview + remote_created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The remote creation date of the interview + remote_updated_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The remote modification date of the interview + field_mappings: + type: object + example: &ref_156 + fav_dish: broccoli + fav_color: red + additionalProperties: true + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the interview + remote_id: + type: string + example: id_1 + nullable: true + description: The remote ID of the interview in the context of the 3rd Party + remote_data: + type: object + example: + fav_dish: broccoli + fav_color: red + nullable: true + additionalProperties: true + description: The remote data of the interview in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The created date of the object + modified_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The modified date of the object + UnifiedAtsInterviewInput: + type: object + properties: + status: + type: string + enum: *ref_154 + example: SCHEDULED + nullable: true + description: The status of the interview + application_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the application + job_interview_stage_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the job interview stage + organized_by: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the organizer + interviewers: + example: *ref_155 + nullable: true + description: The UUIDs of the interviewers + type: array + items: + type: string + location: + type: string + example: San Francisco + nullable: true + description: The location of the interview + start_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The start date and time of the interview + end_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The end date and time of the interview + remote_created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The remote creation date of the interview + remote_updated_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The remote modification date of the interview + field_mappings: + type: object + example: *ref_156 + additionalProperties: true + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + UnifiedAtsJobinterviewstageOutput: + type: object + properties: + name: + type: string + example: Second Call + nullable: true + description: The name of the job interview stage + stage_order: + type: number + example: 1 + nullable: true + description: The order of the stage + job_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the job + field_mappings: + type: object + example: + fav_dish: broccoli + fav_color: red + additionalProperties: true + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the job interview stage + remote_id: + type: string + example: id_1 + nullable: true + description: >- + The remote ID of the job interview stage in the context of the 3rd + Party + remote_data: + type: object + example: + fav_dish: broccoli + fav_color: red + nullable: true + additionalProperties: true + description: >- + The remote data of the job interview stage in the context of the 3rd + Party + created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The created date of the object + modified_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The modified date of the object + UnifiedAtsJobOutput: + type: object + properties: + name: + type: string + example: Financial Analyst + nullable: true + description: The name of the job + description: + type: string + example: Extract financial data and write detailed investment thesis + nullable: true + description: The description of the job + code: + type: string + example: JOB123 + nullable: true + description: The code of the job + status: + type: string + enum: + - OPEN + - CLOSED + - DRAFT + - ARCHIVED + - PENDING + example: OPEN + nullable: true + description: The status of the job + type: + type: string + example: POSTING + enum: + - POSTING + - REQUISITION + - PROFILE + nullable: true + description: The type of the job + confidential: + type: boolean + example: true + nullable: true + description: Whether the job is confidential + departments: + example: + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The departments UUIDs associated with the job + type: array + items: + type: string + offices: + example: + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The offices UUIDs associated with the job + type: array + items: + type: string + managers: + example: + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The managers UUIDs associated with the job + type: array + items: + type: string + recruiters: + example: + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The recruiters UUIDs associated with the job + type: array + items: + type: string + remote_created_at: + type: string + example: '2024-10-01T12:00:00Z' + format: date-time + nullable: true + description: The remote creation date of the job + remote_updated_at: + type: string + example: '2024-10-01T12:00:00Z' + format: date-time + nullable: true + description: The remote modification date of the job + field_mappings: + type: object + example: + fav_dish: broccoli + fav_color: red + additionalProperties: true + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the job + remote_id: + type: string + example: id_1 + nullable: true + description: The remote ID of the job in the context of the 3rd Party + remote_data: + type: object + example: + key1: value1 + key2: 42 + key3: true + nullable: true + additionalProperties: true + description: The remote data of the job in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The created date of the object + modified_at: + format: date-time + type: string + example: '2023-10-01T12:00:00Z' + nullable: true + description: The modified date of the object + UnifiedAtsOfferOutput: + type: object + properties: + created_by: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + description: The UUID of the creator + nullable: true + remote_created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + description: The remote creation date of the offer + nullable: true + closed_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + description: The closing date of the offer + nullable: true + sent_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + description: The sending date of the offer + nullable: true + start_date: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + description: The start date of the offer + nullable: true + status: + type: string + example: DRAFT + enum: + - DRAFT + - APPROVAL_SENT + - APPROVED + - SENT + - SENT_MANUALLY + - OPENED + - DENIED + - SIGNED + - DEPRECATED + description: The status of the offer + nullable: true + application_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + description: The UUID of the application + nullable: true + field_mappings: + type: object + example: + fav_dish: broccoli + fav_color: red + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + nullable: true + additionalProperties: true + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + description: The UUID of the offer + nullable: true + remote_id: + type: string + example: id_1 + description: The remote ID of the offer in the context of the 3rd Party + nullable: true + remote_data: + type: object + example: + fav_dish: broccoli + fav_color: red + description: The remote data of the offer in the context of the 3rd Party + nullable: true + additionalProperties: true + created_at: + type: object + example: '2024-10-01T12:00:00Z' + description: The created date of the object + nullable: true + modified_at: + type: object + example: '2024-10-01T12:00:00Z' + description: The modified date of the object + nullable: true + UnifiedAtsOfficeOutput: + type: object + properties: + name: + type: string + example: Condo Office 5th + nullable: true + description: The name of the office + location: + type: string + example: New York + nullable: true + description: The location of the office + field_mappings: + type: object + example: + fav_dish: broccoli + fav_color: red + additionalProperties: true + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + description: The UUID of the office + remote_id: + type: string + example: id_1 + nullable: true + description: The remote ID of the office in the context of the 3rd Party + remote_data: + type: object + example: + fav_dish: broccoli + fav_color: red + nullable: true + additionalProperties: true + description: The remote data of the office in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The created date of the object + modified_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The modified date of the object + UnifiedAtsRejectreasonOutput: + type: object + properties: + name: + type: string + example: Candidate inexperienced + nullable: true + description: The name of the reject reason + field_mappings: + type: object + example: + fav_dish: broccoli + fav_color: red + additionalProperties: true + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + nullable: true + description: The UUID of the reject reason + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + remote_id: + type: string + nullable: true + description: The remote ID of the reject reason in the context of the 3rd Party + example: id_1 + remote_data: + type: object + example: + fav_dish: broccoli + fav_color: red + nullable: true + additionalProperties: true + description: The remote data of the reject reason in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The created date of the object + modified_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The modified date of the object + UnifiedAtsScorecardOutput: + type: object + properties: + overall_recommendation: + type: string + enum: + - DEFINITELY_NO + - 'NO' + - 'YES' + - STRONG_YES + - NO_DECISION + example: 'YES' + nullable: true + description: The overall recommendation + application_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the application + interview_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the interview + remote_created_at: + type: string + example: '2024-10-01T12:00:00Z' + format: date-time + nullable: true + description: The remote creation date of the scorecard + submitted_at: + type: string + example: '2024-10-01T12:00:00Z' + format: date-time + nullable: true + description: The submission date of the scorecard + field_mappings: + type: object + example: + fav_dish: broccoli + fav_color: red + additionalProperties: true + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + description: The UUID of the scorecard + remote_id: + type: string + example: id_1 + nullable: true + description: The remote ID of the scorecard in the context of the 3rd Party + remote_data: + type: object + example: + fav_dish: broccoli + fav_color: red + nullable: true + additionalProperties: true + description: The remote data of the scorecard in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The created date of the object + modified_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The modified date of the object + UnifiedAtsTagOutput: + type: object + properties: + name: + type: string + example: Important + nullable: true + description: The name of the tag + id_ats_candidate: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the candidate + field_mappings: + type: object + example: + fav_dish: broccoli + fav_color: red + additionalProperties: true + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the tag + remote_id: + type: string + example: id_1 + nullable: true + description: The remote ID of the tag in the context of the 3rd Party + remote_data: + type: object + example: + fav_dish: broccoli + fav_color: red + nullable: true + additionalProperties: true + description: The remote data of the tag in the context of the 3rd Party + created_at: + format: date-time + type: string + nullable: true + example: '2024-10-01T12:00:00Z' + description: The creation date of the tag + modified_at: + format: date-time + type: string + nullable: true + example: '2024-10-01T12:00:00Z' + description: The modification date of the tag + UnifiedAtsUserOutput: + type: object + properties: + first_name: + type: string + example: John + description: The first name of the user + nullable: true + last_name: + type: string + example: Doe + description: The last name of the user + nullable: true + email: + type: string + example: john.doe@example.com + description: The email of the user + nullable: true + disabled: + type: boolean + example: false + description: Whether the user is disabled + nullable: true + access_role: + type: string + example: ADMIN + enum: + - SUPER_ADMIN + - ADMIN + - TEAM_MEMBER + - LIMITED_TEAM_MEMBER + - INTERVIEWER + description: The access role of the user + nullable: true + remote_created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + description: The remote creation date of the user + nullable: true + remote_modified_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + description: The remote modification date of the user + nullable: true + field_mappings: + type: object + example: + fav_dish: broccoli + fav_color: red + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + nullable: true + additionalProperties: true + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + description: The UUID of the user + nullable: true + remote_id: + type: string + example: id_1 + description: The remote ID of the user in the context of the 3rd Party + nullable: true + remote_data: + type: object + example: + fav_dish: broccoli + fav_color: red + description: The remote data of the user in the context of the 3rd Party + nullable: true + additionalProperties: true + created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + description: The created date of the object + nullable: true + modified_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + description: The modified date of the object + nullable: true + UnifiedAtsEeocsOutput: + type: object + properties: + candidate_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the candidate + submitted_at: + type: string + example: '2024-10-01T12:00:00Z' + format: date-time + nullable: true + description: The submission date of the EEOC + race: + type: string + enum: + - AMERICAN_INDIAN_OR_ALASKAN_NATIVE + - ASIAN + - BLACK_OR_AFRICAN_AMERICAN + - HISPANIC_OR_LATINO + - WHITE + - NATIVE_HAWAIIAN_OR_OTHER_PACIFIC_ISLANDER + - TWO_OR_MORE_RACES + - DECLINE_TO_SELF_IDENTIFY + example: AMERICAN_INDIAN_OR_ALASKAN_NATIVE + nullable: true + description: The race of the candidate + gender: + type: string + example: MALE + enum: + - MALE + - FEMALE + - NON_BINARY + - OTHER + - DECLINE_TO_SELF_IDENTIFY + nullable: true + description: The gender of the candidate + veteran_status: + type: string + example: I_AM_NOT_A_PROTECTED_VETERAN + enum: + - I_AM_NOT_A_PROTECTED_VETERAN + - >- + I_IDENTIFY_AS_ONE_OR_MORE_OF_THE_CLASSIFICATIONS_OF_A_PROTECTED_VETERAN + - I_DONT_WISH_TO_ANSWER + nullable: true + description: The veteran status of the candidate + disability_status: + type: string + enum: + - YES_I_HAVE_A_DISABILITY_OR_PREVIOUSLY_HAD_A_DISABILITY + - NO_I_DONT_HAVE_A_DISABILITY + - I_DONT_WISH_TO_ANSWER + example: YES_I_HAVE_A_DISABILITY_OR_PREVIOUSLY_HAD_A_DISABILITY + nullable: true + description: The disability status of the candidate + field_mappings: + type: object + example: + fav_dish: broccoli + fav_color: red + additionalProperties: true + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the EEOC + remote_id: + type: string + example: id_1 + nullable: true + description: The remote ID of the EEOC in the context of the 3rd Party + remote_data: + type: object + example: + fav_dish: broccoli + fav_color: red + nullable: true + additionalProperties: true + description: The remote data of the EEOC in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The created date of the object + modified_at: + format: date-time + type: string + example: '2024-10-01T12:00:00Z' + nullable: true + description: The modified date of the object + UnifiedAccountingAccountOutput: + type: object + properties: + name: + type: string + example: Cash + nullable: true + description: The name of the account + description: + type: string + example: Main cash account for daily operations + nullable: true + description: A description of the account + classification: + type: string + example: Asset + nullable: true + description: The classification of the account + type: + type: string + example: Current Asset + nullable: true + description: The type of the account + status: + type: string + example: Active + nullable: true + description: The status of the account + current_balance: + type: number + example: 10000 + nullable: true + description: The current balance of the account + currency: + type: string + example: USD + enum: &ref_157 + - AED + - AFN + - ALL + - AMD + - ANG + - AOA + - ARS + - AUD + - AWG + - AZN + - BAM + - BBD + - BDT + - BGN + - BHD + - BIF + - BMD + - BND + - BOB + - BRL + - BSD + - BTN + - BWP + - BYN + - BZD + - CAD + - CDF + - CHF + - CLP + - CNY + - COP + - CRC + - CUP + - CVE + - CZK + - DJF + - DKK + - DOP + - DZD + - EGP + - ERN + - ETB + - EUR + - FJD + - FKP + - FOK + - GBP + - GEL + - GGP + - GHS + - GIP + - GMD + - GNF + - GTQ + - GYD + - HKD + - HNL + - HRK + - HTG + - HUF + - IDR + - ILS + - IMP + - INR + - IQD + - IRR + - ISK + - JEP + - JMD + - JOD + - JPY + - KES + - KGS + - KHR + - KID + - KMF + - KRW + - KWD + - KYD + - KZT + - LAK + - LBP + - LKR + - LRD + - LSL + - LYD + - MAD + - MDL + - MGA + - MKD + - MMK + - MNT + - MOP + - MRU + - MUR + - MVR + - MWK + - MXN + - MYR + - MZN + - NAD + - NGN + - NIO + - NOK + - NPR + - NZD + - OMR + - PAB + - PEN + - PGK + - PHP + - PKR + - PLN + - PYG + - QAR + - RON + - RSD + - RUB + - RWF + - SAR + - SBD + - SCR + - SDG + - SEK + - SGD + - SHP + - SLE + - SLL + - SOS + - SRD + - SSP + - STN + - SYP + - SZL + - THB + - TJS + - TMT + - TND + - TOP + - TRY + - TTD + - TVD + - TWD + - TZS + - UAH + - UGX + - USD + - UYU + - UZS + - VES + - VND + - VUV + - WST + - XAF + - XCD + - XDR + - XOF + - XPF + - YER + - ZAR + - ZMW + - ZWL + nullable: true + description: The currency of the account + account_number: + type: string + example: '1000' + nullable: true + description: The account number + parent_account: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the parent account + company_info_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated company info + field_mappings: + type: object + example: &ref_158 + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the account record + remote_id: + type: string + example: account_1234 + nullable: true + description: The remote ID of the account in the context of the 3rd Party + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: The remote data of the account in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The created date of the account record + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The last modified date of the account record + UnifiedAccountingAccountInput: + type: object + properties: + name: + type: string + example: Cash + nullable: true + description: The name of the account + description: + type: string + example: Main cash account for daily operations + nullable: true + description: A description of the account + classification: + type: string + example: Asset + nullable: true + description: The classification of the account + type: + type: string + example: Current Asset + nullable: true + description: The type of the account + status: + type: string + example: Active + nullable: true + description: The status of the account + current_balance: + type: number + example: 10000 + nullable: true + description: The current balance of the account + currency: + type: string + example: USD + enum: *ref_157 + nullable: true + description: The currency of the account + account_number: + type: string + example: '1000' + nullable: true + description: The account number + parent_account: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the parent account + company_info_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated company info + field_mappings: + type: object + example: *ref_158 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + UnifiedAccountingAddressOutput: + type: object + properties: + type: + type: string + example: Billing + nullable: true + description: The type of the address + street_1: + type: string + example: 123 Main St + nullable: true + description: The first line of the street address + street_2: + type: string + example: Apt 4B + nullable: true + description: The second line of the street address + city: + type: string + example: New York + nullable: true + description: The city of the address + state: + type: string + example: NY + nullable: true + description: The state of the address + country_subdivision: + type: string + example: New York + nullable: true + description: The country subdivision (e.g., province or state) of the address + country: + type: string + example: USA + nullable: true + description: The country of the address + zip: + type: string + example: '10001' + nullable: true + description: The zip or postal code of the address + contact_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated contact + company_info_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated company info + field_mappings: + type: object + example: + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the address record + remote_id: + type: string + example: address_1234 + nullable: true + description: The remote ID of the address in the context of the 3rd Party + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: The remote data of the address in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The created date of the address record + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The last modified date of the address record + UnifiedAccountingAttachmentOutput: + type: object + properties: + file_name: + type: string + example: invoice.pdf + nullable: true + description: The name of the attached file + file_url: + type: string + example: https://example.com/files/invoice.pdf + nullable: true + description: The URL where the file can be accessed + account_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated account + field_mappings: + type: object + example: &ref_159 + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the attachment record + remote_id: + type: string + example: attachment_1234 + nullable: true + description: The remote ID of the attachment in the context of the 3rd Party + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: The remote data of the attachment in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The created date of the attachment record + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The last modified date of the attachment record + UnifiedAccountingAttachmentInput: + type: object + properties: + file_name: + type: string + example: invoice.pdf + nullable: true + description: The name of the attached file + file_url: + type: string + example: https://example.com/files/invoice.pdf + nullable: true + description: The URL where the file can be accessed + account_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated account + field_mappings: + type: object + example: *ref_159 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + LineItem: + type: object + properties: + name: + type: string + example: Net Income + nullable: true + description: The name of the report item + value: + type: number + example: 100000 + nullable: true + description: The value of the report item + type: + type: string + example: Operating Activities + nullable: true + description: The type of the report item + parent_item: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the parent item + remote_id: + type: string + example: report_item_1234 + nullable: true + description: The remote ID of the report item + remote_generated_at: + format: date-time + type: string + example: '2024-07-01T12:00:00Z' + nullable: true + description: The date when the report item was generated in the remote system + company_info_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated company info object + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + description: The created date of the report item + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + description: The last modified date of the report item + UnifiedAccountingBalancesheetOutput: + type: object + properties: + name: + type: string + example: Q2 2024 Balance Sheet + nullable: true + description: The name of the balance sheet + currency: + type: string + example: USD + enum: + - AED + - AFN + - ALL + - AMD + - ANG + - AOA + - ARS + - AUD + - AWG + - AZN + - BAM + - BBD + - BDT + - BGN + - BHD + - BIF + - BMD + - BND + - BOB + - BRL + - BSD + - BTN + - BWP + - BYN + - BZD + - CAD + - CDF + - CHF + - CLP + - CNY + - COP + - CRC + - CUP + - CVE + - CZK + - DJF + - DKK + - DOP + - DZD + - EGP + - ERN + - ETB + - EUR + - FJD + - FKP + - FOK + - GBP + - GEL + - GGP + - GHS + - GIP + - GMD + - GNF + - GTQ + - GYD + - HKD + - HNL + - HRK + - HTG + - HUF + - IDR + - ILS + - IMP + - INR + - IQD + - IRR + - ISK + - JEP + - JMD + - JOD + - JPY + - KES + - KGS + - KHR + - KID + - KMF + - KRW + - KWD + - KYD + - KZT + - LAK + - LBP + - LKR + - LRD + - LSL + - LYD + - MAD + - MDL + - MGA + - MKD + - MMK + - MNT + - MOP + - MRU + - MUR + - MVR + - MWK + - MXN + - MYR + - MZN + - NAD + - NGN + - NIO + - NOK + - NPR + - NZD + - OMR + - PAB + - PEN + - PGK + - PHP + - PKR + - PLN + - PYG + - QAR + - RON + - RSD + - RUB + - RWF + - SAR + - SBD + - SCR + - SDG + - SEK + - SGD + - SHP + - SLE + - SLL + - SOS + - SRD + - SSP + - STN + - SYP + - SZL + - THB + - TJS + - TMT + - TND + - TOP + - TRY + - TTD + - TVD + - TWD + - TZS + - UAH + - UGX + - USD + - UYU + - UZS + - VES + - VND + - VUV + - WST + - XAF + - XCD + - XDR + - XOF + - XPF + - YER + - ZAR + - ZMW + - ZWL + nullable: true + description: The currency used in the balance sheet + company_info_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated company info + date: + format: date-time + type: string + example: '2024-06-30T23:59:59Z' + nullable: true + description: The date of the balance sheet + net_assets: + type: number + example: 1000000 + nullable: true + description: The net assets value + assets: + example: + - Cash + - Accounts Receivable + - Inventory + nullable: true + description: The list of assets + type: array + items: + type: string + liabilities: + example: + - Accounts Payable + - Long-term Debt + nullable: true + description: The list of liabilities + type: array + items: + type: string + equity: + example: + - Common Stock + - Retained Earnings + nullable: true + description: The list of equity items + type: array + items: + type: string + remote_generated_at: + format: date-time + type: string + example: '2024-07-01T12:00:00Z' + nullable: true + description: The date when the balance sheet was generated in the remote system + line_items: + description: The report items associated with this balance sheet + type: array + items: + $ref: '#/components/schemas/LineItem' + field_mappings: + type: object + example: + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the balance sheet record + remote_id: + type: string + example: balancesheet_1234 + nullable: true + description: The remote ID of the balance sheet in the context of the 3rd Party + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: The remote data of the balance sheet in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The created date of the balance sheet record + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The last modified date of the balance sheet record + UnifiedAccountingCashflowstatementOutput: + type: object + properties: + name: + type: string + example: Q2 2024 Cash Flow Statement + nullable: true + description: The name of the cash flow statement + currency: + type: string + example: USD + enum: + - AED + - AFN + - ALL + - AMD + - ANG + - AOA + - ARS + - AUD + - AWG + - AZN + - BAM + - BBD + - BDT + - BGN + - BHD + - BIF + - BMD + - BND + - BOB + - BRL + - BSD + - BTN + - BWP + - BYN + - BZD + - CAD + - CDF + - CHF + - CLP + - CNY + - COP + - CRC + - CUP + - CVE + - CZK + - DJF + - DKK + - DOP + - DZD + - EGP + - ERN + - ETB + - EUR + - FJD + - FKP + - FOK + - GBP + - GEL + - GGP + - GHS + - GIP + - GMD + - GNF + - GTQ + - GYD + - HKD + - HNL + - HRK + - HTG + - HUF + - IDR + - ILS + - IMP + - INR + - IQD + - IRR + - ISK + - JEP + - JMD + - JOD + - JPY + - KES + - KGS + - KHR + - KID + - KMF + - KRW + - KWD + - KYD + - KZT + - LAK + - LBP + - LKR + - LRD + - LSL + - LYD + - MAD + - MDL + - MGA + - MKD + - MMK + - MNT + - MOP + - MRU + - MUR + - MVR + - MWK + - MXN + - MYR + - MZN + - NAD + - NGN + - NIO + - NOK + - NPR + - NZD + - OMR + - PAB + - PEN + - PGK + - PHP + - PKR + - PLN + - PYG + - QAR + - RON + - RSD + - RUB + - RWF + - SAR + - SBD + - SCR + - SDG + - SEK + - SGD + - SHP + - SLE + - SLL + - SOS + - SRD + - SSP + - STN + - SYP + - SZL + - THB + - TJS + - TMT + - TND + - TOP + - TRY + - TTD + - TVD + - TWD + - TZS + - UAH + - UGX + - USD + - UYU + - UZS + - VES + - VND + - VUV + - WST + - XAF + - XCD + - XDR + - XOF + - XPF + - YER + - ZAR + - ZMW + - ZWL + nullable: true + description: The currency used in the cash flow statement + company_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated company + start_period: + format: date-time + type: string + example: '2024-04-01T00:00:00Z' + nullable: true + description: The start date of the period covered by the cash flow statement + end_period: + format: date-time + type: string + example: '2024-06-30T23:59:59Z' + nullable: true + description: The end date of the period covered by the cash flow statement + cash_at_beginning_of_period: + type: number + example: 1000000 + nullable: true + description: The cash balance at the beginning of the period + cash_at_end_of_period: + type: number + example: 1200000 + nullable: true + description: The cash balance at the end of the period + remote_generated_at: + format: date-time + type: string + example: '2024-07-01T12:00:00Z' + nullable: true + description: >- + The date when the cash flow statement was generated in the remote + system + line_items: + description: The report items associated with this cash flow statement + type: array + items: + $ref: '#/components/schemas/LineItem' + field_mappings: + type: object + example: + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the cash flow statement record + remote_id: + type: string + example: cashflowstatement_1234 + nullable: true + description: >- + The remote ID of the cash flow statement in the context of the 3rd + Party + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: >- + The remote data of the cash flow statement in the context of the 3rd + Party + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The created date of the cash flow statement record + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The last modified date of the cash flow statement record + UnifiedAccountingCompanyinfoOutput: + type: object + properties: + name: + type: string + example: Acme Corporation + nullable: true + description: The name of the company + legal_name: + type: string + example: Acme Corporation LLC + nullable: true + description: The legal name of the company + tax_number: + type: string + example: '123456789' + nullable: true + description: The tax number of the company + fiscal_year_end_month: + type: number + example: 12 + nullable: true + description: The month of the fiscal year end (1-12) + fiscal_year_end_day: + type: number + example: 31 + nullable: true + description: The day of the fiscal year end (1-31) + currency: + type: string + example: USD + enum: + - AED + - AFN + - ALL + - AMD + - ANG + - AOA + - ARS + - AUD + - AWG + - AZN + - BAM + - BBD + - BDT + - BGN + - BHD + - BIF + - BMD + - BND + - BOB + - BRL + - BSD + - BTN + - BWP + - BYN + - BZD + - CAD + - CDF + - CHF + - CLP + - CNY + - COP + - CRC + - CUP + - CVE + - CZK + - DJF + - DKK + - DOP + - DZD + - EGP + - ERN + - ETB + - EUR + - FJD + - FKP + - FOK + - GBP + - GEL + - GGP + - GHS + - GIP + - GMD + - GNF + - GTQ + - GYD + - HKD + - HNL + - HRK + - HTG + - HUF + - IDR + - ILS + - IMP + - INR + - IQD + - IRR + - ISK + - JEP + - JMD + - JOD + - JPY + - KES + - KGS + - KHR + - KID + - KMF + - KRW + - KWD + - KYD + - KZT + - LAK + - LBP + - LKR + - LRD + - LSL + - LYD + - MAD + - MDL + - MGA + - MKD + - MMK + - MNT + - MOP + - MRU + - MUR + - MVR + - MWK + - MXN + - MYR + - MZN + - NAD + - NGN + - NIO + - NOK + - NPR + - NZD + - OMR + - PAB + - PEN + - PGK + - PHP + - PKR + - PLN + - PYG + - QAR + - RON + - RSD + - RUB + - RWF + - SAR + - SBD + - SCR + - SDG + - SEK + - SGD + - SHP + - SLE + - SLL + - SOS + - SRD + - SSP + - STN + - SYP + - SZL + - THB + - TJS + - TMT + - TND + - TOP + - TRY + - TTD + - TVD + - TWD + - TZS + - UAH + - UGX + - USD + - UYU + - UZS + - VES + - VND + - VUV + - WST + - XAF + - XCD + - XDR + - XOF + - XPF + - YER + - ZAR + - ZMW + - ZWL + nullable: true + description: The currency used by the company + urls: + example: + - https://www.acmecorp.com + - https://store.acmecorp.com + nullable: true + description: The URLs associated with the company + type: array + items: + type: string + tracking_categories: + example: + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUIDs of the tracking categories used by the company + type: array + items: + type: string + field_mappings: + type: object + example: + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the company info record + remote_id: + type: string + example: company_1234 + nullable: true + description: The remote ID of the company info in the context of the 3rd Party + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: The remote data of the company info in the context of the 3rd Party + remote_created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date when the company info was created in the remote system + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The created date of the company info record + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The last modified date of the company info record + UnifiedAccountingContactOutput: + type: object + properties: + name: + type: string + example: John Doe + nullable: true + description: The name of the contact + is_supplier: + type: boolean + example: true + nullable: true + description: Indicates if the contact is a supplier + is_customer: + type: boolean + example: false + nullable: true + description: Indicates if the contact is a customer + email_address: + type: string + example: john.doe@example.com + nullable: true + description: The email address of the contact + tax_number: + type: string + example: '123456789' + nullable: true + description: The tax number of the contact + status: + type: string + example: Active + nullable: true + description: The status of the contact + currency: + type: string + example: USD + nullable: true + enum: &ref_160 + - AED + - AFN + - ALL + - AMD + - ANG + - AOA + - ARS + - AUD + - AWG + - AZN + - BAM + - BBD + - BDT + - BGN + - BHD + - BIF + - BMD + - BND + - BOB + - BRL + - BSD + - BTN + - BWP + - BYN + - BZD + - CAD + - CDF + - CHF + - CLP + - CNY + - COP + - CRC + - CUP + - CVE + - CZK + - DJF + - DKK + - DOP + - DZD + - EGP + - ERN + - ETB + - EUR + - FJD + - FKP + - FOK + - GBP + - GEL + - GGP + - GHS + - GIP + - GMD + - GNF + - GTQ + - GYD + - HKD + - HNL + - HRK + - HTG + - HUF + - IDR + - ILS + - IMP + - INR + - IQD + - IRR + - ISK + - JEP + - JMD + - JOD + - JPY + - KES + - KGS + - KHR + - KID + - KMF + - KRW + - KWD + - KYD + - KZT + - LAK + - LBP + - LKR + - LRD + - LSL + - LYD + - MAD + - MDL + - MGA + - MKD + - MMK + - MNT + - MOP + - MRU + - MUR + - MVR + - MWK + - MXN + - MYR + - MZN + - NAD + - NGN + - NIO + - NOK + - NPR + - NZD + - OMR + - PAB + - PEN + - PGK + - PHP + - PKR + - PLN + - PYG + - QAR + - RON + - RSD + - RUB + - RWF + - SAR + - SBD + - SCR + - SDG + - SEK + - SGD + - SHP + - SLE + - SLL + - SOS + - SRD + - SSP + - STN + - SYP + - SZL + - THB + - TJS + - TMT + - TND + - TOP + - TRY + - TTD + - TVD + - TWD + - TZS + - UAH + - UGX + - USD + - UYU + - UZS + - VES + - VND + - VUV + - WST + - XAF + - XCD + - XDR + - XOF + - XPF + - YER + - ZAR + - ZMW + - ZWL + description: The currency associated with the contact + remote_updated_at: + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date when the contact was last updated in the remote system + company_info_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated company info + field_mappings: + type: object + example: &ref_161 + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the contact record + remote_id: + type: string + example: contact_1234 + nullable: true + description: The remote ID of the contact in the context of the 3rd Party + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: The remote data of the contact in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The created date of the contact record + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The last modified date of the contact record + UnifiedAccountingContactInput: + type: object + properties: + name: + type: string + example: John Doe + nullable: true + description: The name of the contact + is_supplier: + type: boolean + example: true + nullable: true + description: Indicates if the contact is a supplier + is_customer: + type: boolean + example: false + nullable: true + description: Indicates if the contact is a customer + email_address: + type: string + example: john.doe@example.com + nullable: true + description: The email address of the contact + tax_number: + type: string + example: '123456789' + nullable: true + description: The tax number of the contact + status: + type: string + example: Active + nullable: true + description: The status of the contact + currency: + type: string + example: USD + nullable: true + enum: *ref_160 + description: The currency associated with the contact + remote_updated_at: + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date when the contact was last updated in the remote system + company_info_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated company info + field_mappings: + type: object + example: *ref_161 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + UnifiedAccountingCreditnoteOutput: + type: object + properties: + transaction_date: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date of the credit note transaction + status: + type: string + example: Issued + nullable: true + description: The status of the credit note + number: + type: string + example: CN-001 + nullable: true + description: The number of the credit note + contact_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated contact + company_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated company + exchange_rate: + type: string + example: '1.2' + nullable: true + description: The exchange rate applied to the credit note + total_amount: + type: number + example: 10000 + nullable: true + description: The total amount of the credit note + remaining_credit: + type: number + example: 5000 + nullable: true + description: The remaining credit on the credit note + tracking_categories: + example: + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUIDs of the tracking categories associated with the credit note + type: array + items: + type: string + currency: + type: string + example: USD + enum: + - AED + - AFN + - ALL + - AMD + - ANG + - AOA + - ARS + - AUD + - AWG + - AZN + - BAM + - BBD + - BDT + - BGN + - BHD + - BIF + - BMD + - BND + - BOB + - BRL + - BSD + - BTN + - BWP + - BYN + - BZD + - CAD + - CDF + - CHF + - CLP + - CNY + - COP + - CRC + - CUP + - CVE + - CZK + - DJF + - DKK + - DOP + - DZD + - EGP + - ERN + - ETB + - EUR + - FJD + - FKP + - FOK + - GBP + - GEL + - GGP + - GHS + - GIP + - GMD + - GNF + - GTQ + - GYD + - HKD + - HNL + - HRK + - HTG + - HUF + - IDR + - ILS + - IMP + - INR + - IQD + - IRR + - ISK + - JEP + - JMD + - JOD + - JPY + - KES + - KGS + - KHR + - KID + - KMF + - KRW + - KWD + - KYD + - KZT + - LAK + - LBP + - LKR + - LRD + - LSL + - LYD + - MAD + - MDL + - MGA + - MKD + - MMK + - MNT + - MOP + - MRU + - MUR + - MVR + - MWK + - MXN + - MYR + - MZN + - NAD + - NGN + - NIO + - NOK + - NPR + - NZD + - OMR + - PAB + - PEN + - PGK + - PHP + - PKR + - PLN + - PYG + - QAR + - RON + - RSD + - RUB + - RWF + - SAR + - SBD + - SCR + - SDG + - SEK + - SGD + - SHP + - SLE + - SLL + - SOS + - SRD + - SSP + - STN + - SYP + - SZL + - THB + - TJS + - TMT + - TND + - TOP + - TRY + - TTD + - TVD + - TWD + - TZS + - UAH + - UGX + - USD + - UYU + - UZS + - VES + - VND + - VUV + - WST + - XAF + - XCD + - XDR + - XOF + - XPF + - YER + - ZAR + - ZMW + - ZWL + nullable: true + description: The currency of the credit note + payments: + example: + - PAYMENT-001 + - PAYMENT-002 + nullable: true + description: The payments associated with the credit note + type: array + items: + type: string + applied_payments: + example: + - APPLIED-001 + - APPLIED-002 + nullable: true + description: The applied payments associated with the credit note + type: array + items: + type: string + accounting_period_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated accounting period + field_mappings: + type: object + example: + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the credit note record + remote_id: + type: string + example: creditnote_1234 + nullable: true + description: The remote ID of the credit note in the context of the 3rd Party + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: The remote data of the credit note in the context of the 3rd Party + remote_created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date when the credit note was created in the remote system + remote_updated_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date when the credit note was last updated in the remote system + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The created date of the credit note record + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The last modified date of the credit note record + UnifiedAccountingExpenseOutput: + type: object + properties: + transaction_date: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date of the expense transaction + total_amount: + type: number + example: 10000 + nullable: true + description: The total amount of the expense + sub_total: + type: number + example: 9000 + nullable: true + description: The sub-total amount of the expense (before tax) + total_tax_amount: + type: number + example: 1000 + nullable: true + description: The total tax amount of the expense + currency: + type: string + example: USD + enum: &ref_162 + - AED + - AFN + - ALL + - AMD + - ANG + - AOA + - ARS + - AUD + - AWG + - AZN + - BAM + - BBD + - BDT + - BGN + - BHD + - BIF + - BMD + - BND + - BOB + - BRL + - BSD + - BTN + - BWP + - BYN + - BZD + - CAD + - CDF + - CHF + - CLP + - CNY + - COP + - CRC + - CUP + - CVE + - CZK + - DJF + - DKK + - DOP + - DZD + - EGP + - ERN + - ETB + - EUR + - FJD + - FKP + - FOK + - GBP + - GEL + - GGP + - GHS + - GIP + - GMD + - GNF + - GTQ + - GYD + - HKD + - HNL + - HRK + - HTG + - HUF + - IDR + - ILS + - IMP + - INR + - IQD + - IRR + - ISK + - JEP + - JMD + - JOD + - JPY + - KES + - KGS + - KHR + - KID + - KMF + - KRW + - KWD + - KYD + - KZT + - LAK + - LBP + - LKR + - LRD + - LSL + - LYD + - MAD + - MDL + - MGA + - MKD + - MMK + - MNT + - MOP + - MRU + - MUR + - MVR + - MWK + - MXN + - MYR + - MZN + - NAD + - NGN + - NIO + - NOK + - NPR + - NZD + - OMR + - PAB + - PEN + - PGK + - PHP + - PKR + - PLN + - PYG + - QAR + - RON + - RSD + - RUB + - RWF + - SAR + - SBD + - SCR + - SDG + - SEK + - SGD + - SHP + - SLE + - SLL + - SOS + - SRD + - SSP + - STN + - SYP + - SZL + - THB + - TJS + - TMT + - TND + - TOP + - TRY + - TTD + - TVD + - TWD + - TZS + - UAH + - UGX + - USD + - UYU + - UZS + - VES + - VND + - VUV + - WST + - XAF + - XCD + - XDR + - XOF + - XPF + - YER + - ZAR + - ZMW + - ZWL + nullable: true + description: The currency of the expense + exchange_rate: + type: string + example: '1.2' + nullable: true + description: The exchange rate applied to the expense + memo: + type: string + example: Business lunch with client + nullable: true + description: A memo or description for the expense + account_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated account + contact_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated contact + company_info_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated company info + tracking_categories: + example: &ref_163 + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUIDs of the tracking categories associated with the expense + type: array + items: + type: string + line_items: + description: The line items associated with this expense + type: array + items: + $ref: '#/components/schemas/LineItem' + field_mappings: + type: object + example: &ref_164 + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the expense record + remote_id: + type: string + example: expense_1234 + nullable: true + description: The remote ID of the expense in the context of the 3rd Party + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: The remote data of the expense in the context of the 3rd Party + remote_created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date when the expense was created in the remote system + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The created date of the expense record + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The last modified date of the expense record + UnifiedAccountingExpenseInput: + type: object + properties: + transaction_date: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date of the expense transaction + total_amount: + type: number + example: 10000 + nullable: true + description: The total amount of the expense + sub_total: + type: number + example: 9000 + nullable: true + description: The sub-total amount of the expense (before tax) + total_tax_amount: + type: number + example: 1000 + nullable: true + description: The total tax amount of the expense + currency: + type: string + example: USD + enum: *ref_162 + nullable: true + description: The currency of the expense + exchange_rate: + type: string + example: '1.2' + nullable: true + description: The exchange rate applied to the expense + memo: + type: string + example: Business lunch with client + nullable: true + description: A memo or description for the expense + account_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated account + contact_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated contact + company_info_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated company info + tracking_categories: + example: *ref_163 + nullable: true + description: The UUIDs of the tracking categories associated with the expense + type: array + items: + type: string + line_items: + description: The line items associated with this expense + type: array + items: + $ref: '#/components/schemas/LineItem' + field_mappings: + type: object + example: *ref_164 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + UnifiedAccountingIncomestatementOutput: + type: object + properties: + name: + type: string + example: Q2 2024 Income Statement + nullable: true + description: The name of the income statement + currency: + type: string + example: USD + enum: + - AED + - AFN + - ALL + - AMD + - ANG + - AOA + - ARS + - AUD + - AWG + - AZN + - BAM + - BBD + - BDT + - BGN + - BHD + - BIF + - BMD + - BND + - BOB + - BRL + - BSD + - BTN + - BWP + - BYN + - BZD + - CAD + - CDF + - CHF + - CLP + - CNY + - COP + - CRC + - CUP + - CVE + - CZK + - DJF + - DKK + - DOP + - DZD + - EGP + - ERN + - ETB + - EUR + - FJD + - FKP + - FOK + - GBP + - GEL + - GGP + - GHS + - GIP + - GMD + - GNF + - GTQ + - GYD + - HKD + - HNL + - HRK + - HTG + - HUF + - IDR + - ILS + - IMP + - INR + - IQD + - IRR + - ISK + - JEP + - JMD + - JOD + - JPY + - KES + - KGS + - KHR + - KID + - KMF + - KRW + - KWD + - KYD + - KZT + - LAK + - LBP + - LKR + - LRD + - LSL + - LYD + - MAD + - MDL + - MGA + - MKD + - MMK + - MNT + - MOP + - MRU + - MUR + - MVR + - MWK + - MXN + - MYR + - MZN + - NAD + - NGN + - NIO + - NOK + - NPR + - NZD + - OMR + - PAB + - PEN + - PGK + - PHP + - PKR + - PLN + - PYG + - QAR + - RON + - RSD + - RUB + - RWF + - SAR + - SBD + - SCR + - SDG + - SEK + - SGD + - SHP + - SLE + - SLL + - SOS + - SRD + - SSP + - STN + - SYP + - SZL + - THB + - TJS + - TMT + - TND + - TOP + - TRY + - TTD + - TVD + - TWD + - TZS + - UAH + - UGX + - USD + - UYU + - UZS + - VES + - VND + - VUV + - WST + - XAF + - XCD + - XDR + - XOF + - XPF + - YER + - ZAR + - ZMW + - ZWL + nullable: true + description: The currency used in the income statement + start_period: + format: date-time + type: string + example: '2024-04-01T00:00:00Z' + nullable: true + description: The start date of the period covered by the income statement + end_period: + format: date-time + type: string + example: '2024-06-30T23:59:59Z' + nullable: true + description: The end date of the period covered by the income statement + gross_profit: + type: number + example: 1000000 + nullable: true + description: The gross profit for the period + net_operating_income: + type: number + example: 800000 + nullable: true + description: The net operating income for the period + net_income: + type: number + example: 750000 + nullable: true + description: The net income for the period + field_mappings: + type: object + example: + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the income statement record + remote_id: + type: string + example: incomestatement_1234 + nullable: true + description: >- + The remote ID of the income statement in the context of the 3rd + Party + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: >- + The remote data of the income statement in the context of the 3rd + Party + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The created date of the income statement record + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The last modified date of the income statement record + UnifiedAccountingInvoiceOutput: + type: object + properties: + type: + type: string + example: Sales + nullable: true + description: The type of the invoice + number: + type: string + example: INV-001 + nullable: true + description: The invoice number + issue_date: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date the invoice was issued + due_date: + format: date-time + type: string + example: '2024-07-15T12:00:00Z' + nullable: true + description: The due date of the invoice + paid_on_date: + format: date-time + type: string + example: '2024-07-10T12:00:00Z' + nullable: true + description: The date the invoice was paid + memo: + type: string + example: Payment for services rendered + nullable: true + description: A memo or note on the invoice + currency: + type: string + example: USD + enum: &ref_165 + - AED + - AFN + - ALL + - AMD + - ANG + - AOA + - ARS + - AUD + - AWG + - AZN + - BAM + - BBD + - BDT + - BGN + - BHD + - BIF + - BMD + - BND + - BOB + - BRL + - BSD + - BTN + - BWP + - BYN + - BZD + - CAD + - CDF + - CHF + - CLP + - CNY + - COP + - CRC + - CUP + - CVE + - CZK + - DJF + - DKK + - DOP + - DZD + - EGP + - ERN + - ETB + - EUR + - FJD + - FKP + - FOK + - GBP + - GEL + - GGP + - GHS + - GIP + - GMD + - GNF + - GTQ + - GYD + - HKD + - HNL + - HRK + - HTG + - HUF + - IDR + - ILS + - IMP + - INR + - IQD + - IRR + - ISK + - JEP + - JMD + - JOD + - JPY + - KES + - KGS + - KHR + - KID + - KMF + - KRW + - KWD + - KYD + - KZT + - LAK + - LBP + - LKR + - LRD + - LSL + - LYD + - MAD + - MDL + - MGA + - MKD + - MMK + - MNT + - MOP + - MRU + - MUR + - MVR + - MWK + - MXN + - MYR + - MZN + - NAD + - NGN + - NIO + - NOK + - NPR + - NZD + - OMR + - PAB + - PEN + - PGK + - PHP + - PKR + - PLN + - PYG + - QAR + - RON + - RSD + - RUB + - RWF + - SAR + - SBD + - SCR + - SDG + - SEK + - SGD + - SHP + - SLE + - SLL + - SOS + - SRD + - SSP + - STN + - SYP + - SZL + - THB + - TJS + - TMT + - TND + - TOP + - TRY + - TTD + - TVD + - TWD + - TZS + - UAH + - UGX + - USD + - UYU + - UZS + - VES + - VND + - VUV + - WST + - XAF + - XCD + - XDR + - XOF + - XPF + - YER + - ZAR + - ZMW + - ZWL + nullable: true + description: The currency of the invoice + exchange_rate: + type: string + example: '1.2' + nullable: true + description: The exchange rate applied to the invoice + total_discount: + type: number + example: 1000 + nullable: true + description: The total discount applied to the invoice + sub_total: + type: number + example: 10000 + nullable: true + description: The subtotal of the invoice + status: + type: string + example: Paid + nullable: true + description: The status of the invoice + total_tax_amount: + type: number + example: 1000 + nullable: true + description: The total tax amount on the invoice + total_amount: + type: number + example: 11000 + nullable: true + description: The total amount of the invoice + balance: + type: number + example: 0 + nullable: true + description: The remaining balance on the invoice + contact_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated contact + accounting_period_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated accounting period + tracking_categories: + example: &ref_166 + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUIDs of the tracking categories associated with the invoice + type: array + items: + type: string + line_items: + description: The line items associated with this invoice + type: array + items: + $ref: '#/components/schemas/LineItem' + field_mappings: + type: object + example: &ref_167 + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the invoice record + remote_id: + type: string + example: invoice_1234 + nullable: true + description: The remote ID of the invoice in the context of the 3rd Party + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: The remote data of the invoice in the context of the 3rd Party + remote_updated_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date when the invoice was last updated in the remote system + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The created date of the invoice record + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The last modified date of the invoice record + UnifiedAccountingInvoiceInput: + type: object + properties: + type: + type: string + example: Sales + nullable: true + description: The type of the invoice + number: + type: string + example: INV-001 + nullable: true + description: The invoice number + issue_date: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date the invoice was issued + due_date: + format: date-time + type: string + example: '2024-07-15T12:00:00Z' + nullable: true + description: The due date of the invoice + paid_on_date: + format: date-time + type: string + example: '2024-07-10T12:00:00Z' + nullable: true + description: The date the invoice was paid + memo: + type: string + example: Payment for services rendered + nullable: true + description: A memo or note on the invoice + currency: + type: string + example: USD + enum: *ref_165 + nullable: true + description: The currency of the invoice + exchange_rate: + type: string + example: '1.2' + nullable: true + description: The exchange rate applied to the invoice + total_discount: + type: number + example: 1000 + nullable: true + description: The total discount applied to the invoice + sub_total: + type: number + example: 10000 + nullable: true + description: The subtotal of the invoice + status: + type: string + example: Paid + nullable: true + description: The status of the invoice + total_tax_amount: + type: number + example: 1000 + nullable: true + description: The total tax amount on the invoice + total_amount: + type: number + example: 11000 + nullable: true + description: The total amount of the invoice + balance: + type: number + example: 0 + nullable: true + description: The remaining balance on the invoice + contact_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated contact + accounting_period_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated accounting period + tracking_categories: + example: *ref_166 + nullable: true + description: The UUIDs of the tracking categories associated with the invoice + type: array + items: + type: string + line_items: + description: The line items associated with this invoice + type: array + items: + $ref: '#/components/schemas/LineItem' + field_mappings: + type: object + example: *ref_167 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + UnifiedAccountingItemOutput: + type: object + properties: + name: + type: string + example: Product A + nullable: true + description: The name of the accounting item + status: + type: string + example: Active + nullable: true + description: The status of the accounting item + unit_price: + type: number + example: 1000 + nullable: true + description: The unit price of the item in cents + purchase_price: + type: number + example: 800 + nullable: true + description: The purchase price of the item in cents + sales_account: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated sales account + purchase_account: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated purchase account + company_info_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated company info + field_mappings: + type: object + example: + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the accounting item record + remote_id: + type: string + example: item_1234 + nullable: true + description: The remote ID of the item in the context of the 3rd Party + remote_updated_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date when the item was last updated in the remote system + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: The remote data of the item in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The created date of the accounting item record + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The last modified date of the accounting item record + UnifiedAccountingJournalentryOutput: + type: object + properties: + transaction_date: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date of the transaction + payments: + example: &ref_168 + - payment1 + - payment2 + nullable: true + description: The payments associated with the journal entry + type: array + items: + type: string + applied_payments: + example: &ref_169 + - appliedPayment1 + - appliedPayment2 + nullable: true + description: The applied payments for the journal entry + type: array + items: + type: string + memo: + type: string + example: Monthly expense journal entry + nullable: true + description: A memo or note for the journal entry + currency: + type: string + example: USD + enum: &ref_170 + - AED + - AFN + - ALL + - AMD + - ANG + - AOA + - ARS + - AUD + - AWG + - AZN + - BAM + - BBD + - BDT + - BGN + - BHD + - BIF + - BMD + - BND + - BOB + - BRL + - BSD + - BTN + - BWP + - BYN + - BZD + - CAD + - CDF + - CHF + - CLP + - CNY + - COP + - CRC + - CUP + - CVE + - CZK + - DJF + - DKK + - DOP + - DZD + - EGP + - ERN + - ETB + - EUR + - FJD + - FKP + - FOK + - GBP + - GEL + - GGP + - GHS + - GIP + - GMD + - GNF + - GTQ + - GYD + - HKD + - HNL + - HRK + - HTG + - HUF + - IDR + - ILS + - IMP + - INR + - IQD + - IRR + - ISK + - JEP + - JMD + - JOD + - JPY + - KES + - KGS + - KHR + - KID + - KMF + - KRW + - KWD + - KYD + - KZT + - LAK + - LBP + - LKR + - LRD + - LSL + - LYD + - MAD + - MDL + - MGA + - MKD + - MMK + - MNT + - MOP + - MRU + - MUR + - MVR + - MWK + - MXN + - MYR + - MZN + - NAD + - NGN + - NIO + - NOK + - NPR + - NZD + - OMR + - PAB + - PEN + - PGK + - PHP + - PKR + - PLN + - PYG + - QAR + - RON + - RSD + - RUB + - RWF + - SAR + - SBD + - SCR + - SDG + - SEK + - SGD + - SHP + - SLE + - SLL + - SOS + - SRD + - SSP + - STN + - SYP + - SZL + - THB + - TJS + - TMT + - TND + - TOP + - TRY + - TTD + - TVD + - TWD + - TZS + - UAH + - UGX + - USD + - UYU + - UZS + - VES + - VND + - VUV + - WST + - XAF + - XCD + - XDR + - XOF + - XPF + - YER + - ZAR + - ZMW + - ZWL + nullable: true + description: The currency of the journal entry + exchange_rate: + type: string + example: '1.2' + nullable: true + description: The exchange rate applied to the journal entry + id_acc_company_info: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: false + description: The UUID of the associated company info + journal_number: + type: string + example: JE-001 + nullable: true + description: The journal number + tracking_categories: + example: &ref_171 + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: >- + The UUIDs of the tracking categories associated with the journal + entry + type: array + items: + type: string + id_acc_accounting_period: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated accounting period + posting_status: + type: string + example: Posted + nullable: true + description: The posting status of the journal entry + line_items: + description: The line items associated with this journal entry + type: array + items: + $ref: '#/components/schemas/LineItem' + field_mappings: + type: object + example: &ref_172 + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the journal entry record + remote_id: + type: string + example: journal_entry_1234 + nullable: false + description: The remote ID of the journal entry in the context of the 3rd Party + remote_created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date when the journal entry was created in the remote system + remote_modiified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: >- + The date when the journal entry was last modified in the remote + system + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: The remote data of the journal entry in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The created date of the journal entry record + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The last modified date of the journal entry record + UnifiedAccountingJournalentryInput: + type: object + properties: + transaction_date: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date of the transaction + payments: + example: *ref_168 + nullable: true + description: The payments associated with the journal entry + type: array + items: + type: string + applied_payments: + example: *ref_169 + nullable: true + description: The applied payments for the journal entry + type: array + items: + type: string + memo: + type: string + example: Monthly expense journal entry + nullable: true + description: A memo or note for the journal entry + currency: + type: string + example: USD + enum: *ref_170 + nullable: true + description: The currency of the journal entry + exchange_rate: + type: string + example: '1.2' + nullable: true + description: The exchange rate applied to the journal entry + id_acc_company_info: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: false + description: The UUID of the associated company info + journal_number: + type: string + example: JE-001 + nullable: true + description: The journal number + tracking_categories: + example: *ref_171 + nullable: true + description: >- + The UUIDs of the tracking categories associated with the journal + entry + type: array + items: + type: string + id_acc_accounting_period: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated accounting period + posting_status: + type: string + example: Posted + nullable: true + description: The posting status of the journal entry + line_items: + description: The line items associated with this journal entry + type: array + items: + $ref: '#/components/schemas/LineItem' + field_mappings: + type: object + example: *ref_172 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + UnifiedAccountingPaymentOutput: + type: object + properties: + invoice_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated invoice + transaction_date: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date of the transaction + contact_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated contact + account_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated account + currency: + type: string + example: USD + enum: &ref_173 + - AED + - AFN + - ALL + - AMD + - ANG + - AOA + - ARS + - AUD + - AWG + - AZN + - BAM + - BBD + - BDT + - BGN + - BHD + - BIF + - BMD + - BND + - BOB + - BRL + - BSD + - BTN + - BWP + - BYN + - BZD + - CAD + - CDF + - CHF + - CLP + - CNY + - COP + - CRC + - CUP + - CVE + - CZK + - DJF + - DKK + - DOP + - DZD + - EGP + - ERN + - ETB + - EUR + - FJD + - FKP + - FOK + - GBP + - GEL + - GGP + - GHS + - GIP + - GMD + - GNF + - GTQ + - GYD + - HKD + - HNL + - HRK + - HTG + - HUF + - IDR + - ILS + - IMP + - INR + - IQD + - IRR + - ISK + - JEP + - JMD + - JOD + - JPY + - KES + - KGS + - KHR + - KID + - KMF + - KRW + - KWD + - KYD + - KZT + - LAK + - LBP + - LKR + - LRD + - LSL + - LYD + - MAD + - MDL + - MGA + - MKD + - MMK + - MNT + - MOP + - MRU + - MUR + - MVR + - MWK + - MXN + - MYR + - MZN + - NAD + - NGN + - NIO + - NOK + - NPR + - NZD + - OMR + - PAB + - PEN + - PGK + - PHP + - PKR + - PLN + - PYG + - QAR + - RON + - RSD + - RUB + - RWF + - SAR + - SBD + - SCR + - SDG + - SEK + - SGD + - SHP + - SLE + - SLL + - SOS + - SRD + - SSP + - STN + - SYP + - SZL + - THB + - TJS + - TMT + - TND + - TOP + - TRY + - TTD + - TVD + - TWD + - TZS + - UAH + - UGX + - USD + - UYU + - UZS + - VES + - VND + - VUV + - WST + - XAF + - XCD + - XDR + - XOF + - XPF + - YER + - ZAR + - ZMW + - ZWL + nullable: true + description: The currency of the payment + exchange_rate: + type: string + example: '1.2' + nullable: true + description: The exchange rate applied to the payment + total_amount: + type: number + example: 10000 + nullable: true + description: The total amount of the payment in cents + type: + type: string + example: Credit Card + nullable: true + description: The type of payment + company_info_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated company info + accounting_period_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated accounting period + tracking_categories: + example: &ref_174 + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUIDs of the tracking categories associated with the payment + type: array + items: + type: string + line_items: + description: The line items associated with this payment + type: array + items: + $ref: '#/components/schemas/LineItem' + field_mappings: + type: object + example: &ref_175 + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the payment record + remote_id: + type: string + example: payment_1234 + nullable: true + description: The remote ID of the payment in the context of the 3rd Party + remote_updated_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date when the payment was last updated in the remote system + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: The remote data of the payment in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The created date of the payment record + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The last modified date of the payment record + UnifiedAccountingPaymentInput: + type: object + properties: + invoice_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated invoice + transaction_date: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date of the transaction + contact_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated contact + account_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated account + currency: + type: string + example: USD + enum: *ref_173 + nullable: true + description: The currency of the payment + exchange_rate: + type: string + example: '1.2' + nullable: true + description: The exchange rate applied to the payment + total_amount: + type: number + example: 10000 + nullable: true + description: The total amount of the payment in cents + type: + type: string + example: Credit Card + nullable: true + description: The type of payment + company_info_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated company info + accounting_period_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated accounting period + tracking_categories: + example: *ref_174 + nullable: true + description: The UUIDs of the tracking categories associated with the payment + type: array + items: + type: string + line_items: + description: The line items associated with this payment + type: array + items: + $ref: '#/components/schemas/LineItem' + field_mappings: + type: object + example: *ref_175 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + UnifiedAccountingPhonenumberOutput: + type: object + properties: + number: + type: string + example: '+1234567890' + nullable: true + description: The phone number + type: + type: string + example: Mobile + nullable: true + description: The type of phone number + company_info_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated company info + contact_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: false + description: The UUID of the associated contact + field_mappings: + type: object + example: + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the phone number record + remote_id: + type: string + example: phone_1234 + nullable: true + description: The remote ID of the phone number in the context of the 3rd Party + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: The remote data of the phone number in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The created date of the phone number record + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The last modified date of the phone number record + UnifiedAccountingPurchaseorderOutput: + type: object + properties: + status: + type: string + example: Pending + nullable: true + description: The status of the purchase order + issue_date: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The issue date of the purchase order + purchase_order_number: + type: string + example: PO-001 + nullable: true + description: The purchase order number + delivery_date: + format: date-time + type: string + example: '2024-07-15T12:00:00Z' + nullable: true + description: The delivery date for the purchase order + delivery_address: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the delivery address + customer: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the customer + vendor: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the vendor + memo: + type: string + example: Purchase order for Q3 inventory + nullable: true + description: A memo or note for the purchase order + company_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the company + total_amount: + type: number + example: 100000 + nullable: true + description: The total amount of the purchase order in cents + currency: + type: string + example: USD + enum: &ref_176 + - AED + - AFN + - ALL + - AMD + - ANG + - AOA + - ARS + - AUD + - AWG + - AZN + - BAM + - BBD + - BDT + - BGN + - BHD + - BIF + - BMD + - BND + - BOB + - BRL + - BSD + - BTN + - BWP + - BYN + - BZD + - CAD + - CDF + - CHF + - CLP + - CNY + - COP + - CRC + - CUP + - CVE + - CZK + - DJF + - DKK + - DOP + - DZD + - EGP + - ERN + - ETB + - EUR + - FJD + - FKP + - FOK + - GBP + - GEL + - GGP + - GHS + - GIP + - GMD + - GNF + - GTQ + - GYD + - HKD + - HNL + - HRK + - HTG + - HUF + - IDR + - ILS + - IMP + - INR + - IQD + - IRR + - ISK + - JEP + - JMD + - JOD + - JPY + - KES + - KGS + - KHR + - KID + - KMF + - KRW + - KWD + - KYD + - KZT + - LAK + - LBP + - LKR + - LRD + - LSL + - LYD + - MAD + - MDL + - MGA + - MKD + - MMK + - MNT + - MOP + - MRU + - MUR + - MVR + - MWK + - MXN + - MYR + - MZN + - NAD + - NGN + - NIO + - NOK + - NPR + - NZD + - OMR + - PAB + - PEN + - PGK + - PHP + - PKR + - PLN + - PYG + - QAR + - RON + - RSD + - RUB + - RWF + - SAR + - SBD + - SCR + - SDG + - SEK + - SGD + - SHP + - SLE + - SLL + - SOS + - SRD + - SSP + - STN + - SYP + - SZL + - THB + - TJS + - TMT + - TND + - TOP + - TRY + - TTD + - TVD + - TWD + - TZS + - UAH + - UGX + - USD + - UYU + - UZS + - VES + - VND + - VUV + - WST + - XAF + - XCD + - XDR + - XOF + - XPF + - YER + - ZAR + - ZMW + - ZWL + nullable: true + description: The currency of the purchase order + exchange_rate: + type: string + example: '1.2' + nullable: true + description: The exchange rate applied to the purchase order + tracking_categories: + example: &ref_177 + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: >- + The UUIDs of the tracking categories associated with the purchase + order + type: array + items: + type: string + accounting_period_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated accounting period + line_items: + description: The line items associated with this purchase order + type: array + items: + $ref: '#/components/schemas/LineItem' + field_mappings: + type: object + example: &ref_178 + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the purchase order record + remote_id: + type: string + example: po_1234 + nullable: true + description: The remote ID of the purchase order in the context of the 3rd Party + remote_created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date when the purchase order was created in the remote system + remote_updated_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: >- + The date when the purchase order was last updated in the remote + system + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: >- + The remote data of the purchase order in the context of the 3rd + Party + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The created date of the purchase order record + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The last modified date of the purchase order record + UnifiedAccountingPurchaseorderInput: + type: object + properties: + status: + type: string + example: Pending + nullable: true + description: The status of the purchase order + issue_date: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The issue date of the purchase order + purchase_order_number: + type: string + example: PO-001 + nullable: true + description: The purchase order number + delivery_date: + format: date-time + type: string + example: '2024-07-15T12:00:00Z' + nullable: true + description: The delivery date for the purchase order + delivery_address: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the delivery address + customer: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the customer + vendor: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the vendor + memo: + type: string + example: Purchase order for Q3 inventory + nullable: true + description: A memo or note for the purchase order + company_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the company + total_amount: + type: number + example: 100000 + nullable: true + description: The total amount of the purchase order in cents + currency: + type: string + example: USD + enum: *ref_176 + nullable: true + description: The currency of the purchase order + exchange_rate: + type: string + example: '1.2' + nullable: true + description: The exchange rate applied to the purchase order + tracking_categories: + example: *ref_177 + nullable: true + description: >- + The UUIDs of the tracking categories associated with the purchase + order + type: array + items: + type: string + accounting_period_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated accounting period + line_items: + description: The line items associated with this purchase order + type: array + items: + $ref: '#/components/schemas/LineItem' + field_mappings: + type: object + example: *ref_178 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + UnifiedAccountingTaxrateOutput: + type: object + properties: + description: + type: string + example: VAT 20% + nullable: true + description: The description of the tax rate + total_tax_ratge: + type: number + example: 2000 + nullable: true + description: The total tax rate in basis points (e.g., 2000 for 20%) + effective_tax_rate: + type: number + example: 1900 + nullable: true + description: The effective tax rate in basis points (e.g., 1900 for 19%) + company_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated company + field_mappings: + type: object + example: + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the tax rate record + remote_id: + type: string + example: tax_rate_1234 + nullable: true + description: The remote ID of the tax rate in the context of the 3rd Party + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: The remote data of the tax rate in the context of the 3rd Party + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The created date of the tax rate record + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The last modified date of the tax rate record + UnifiedAccountingTrackingcategoryOutput: + type: object + properties: + name: + type: string + example: Department + nullable: true + description: The name of the tracking category + status: + type: string + example: Active + nullable: true + description: The status of the tracking category + category_type: + type: string + example: Expense + nullable: true + description: The type of the tracking category + parent_category: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the parent category, if applicable + field_mappings: + type: object + example: + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the tracking category record + remote_id: + type: string + example: tracking_category_1234 + nullable: true + description: >- + The remote ID of the tracking category in the context of the 3rd + Party + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: >- + The remote data of the tracking category in the context of the 3rd + Party + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The created date of the tracking category record + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The last modified date of the tracking category record + UnifiedAccountingTransactionOutput: + type: object + properties: + transaction_type: + type: string + example: Sale + nullable: true + description: The type of the transaction + number: + type: string + example: '1001' + nullable: true + description: The transaction number + transaction_date: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date of the transaction + total_amount: + type: string + example: '1000' + nullable: true + description: The total amount of the transaction + exchange_rate: + type: string + example: '1.2' + nullable: true + description: The exchange rate applied to the transaction + currency: + type: string + example: USD + enum: + - AED + - AFN + - ALL + - AMD + - ANG + - AOA + - ARS + - AUD + - AWG + - AZN + - BAM + - BBD + - BDT + - BGN + - BHD + - BIF + - BMD + - BND + - BOB + - BRL + - BSD + - BTN + - BWP + - BYN + - BZD + - CAD + - CDF + - CHF + - CLP + - CNY + - COP + - CRC + - CUP + - CVE + - CZK + - DJF + - DKK + - DOP + - DZD + - EGP + - ERN + - ETB + - EUR + - FJD + - FKP + - FOK + - GBP + - GEL + - GGP + - GHS + - GIP + - GMD + - GNF + - GTQ + - GYD + - HKD + - HNL + - HRK + - HTG + - HUF + - IDR + - ILS + - IMP + - INR + - IQD + - IRR + - ISK + - JEP + - JMD + - JOD + - JPY + - KES + - KGS + - KHR + - KID + - KMF + - KRW + - KWD + - KYD + - KZT + - LAK + - LBP + - LKR + - LRD + - LSL + - LYD + - MAD + - MDL + - MGA + - MKD + - MMK + - MNT + - MOP + - MRU + - MUR + - MVR + - MWK + - MXN + - MYR + - MZN + - NAD + - NGN + - NIO + - NOK + - NPR + - NZD + - OMR + - PAB + - PEN + - PGK + - PHP + - PKR + - PLN + - PYG + - QAR + - RON + - RSD + - RUB + - RWF + - SAR + - SBD + - SCR + - SDG + - SEK + - SGD + - SHP + - SLE + - SLL + - SOS + - SRD + - SSP + - STN + - SYP + - SZL + - THB + - TJS + - TMT + - TND + - TOP + - TRY + - TTD + - TVD + - TWD + - TZS + - UAH + - UGX + - USD + - UYU + - UZS + - VES + - VND + - VUV + - WST + - XAF + - XCD + - XDR + - XOF + - XPF + - YER + - ZAR + - ZMW + - ZWL + nullable: true + description: The currency of the transaction + tracking_categories: + example: + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUIDs of the tracking categories associated with the transaction + type: array + items: + type: string + account_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated account + contact_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated contact + company_info_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated company info + accounting_period_id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the associated accounting period + line_items: + description: The line items associated with this transaction + type: array + items: + $ref: '#/components/schemas/LineItem' + field_mappings: + type: object + example: + custom_field_1: value1 + custom_field_2: value2 + nullable: true + description: >- + The custom field mappings of the object between the remote 3rd party + & Panora + id: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the transaction record + remote_id: + type: string + example: remote_id_1234 + nullable: false + description: The remote ID of the transaction + created_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: false + description: The created date of the transaction + remote_data: + type: object + example: + raw_data: + additional_field: some value + nullable: true + description: >- + The remote data of the tracking category in the context of the 3rd + Party + modified_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: false + description: The last modified date of the transaction + remote_updated_at: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date when the transaction was last updated in the remote system + UnifiedAccountingVendorcreditOutput: + type: object + properties: + number: + type: string + example: VC-001 + nullable: true + description: The number of the vendor credit + transaction_date: + format: date-time + type: string + example: '2024-06-15T12:00:00Z' + nullable: true + description: The date of the transaction + vendor: + type: string + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f + nullable: true + description: The UUID of the vendor associated with the credit + total_amount: + type: string + example: '1000' + nullable: true + description: The total amount of the vendor credit + currency: + type: string + example: USD + nullable: true + enum: + - AED + - AFN + - ALL + - AMD + - ANG + - AOA + - ARS + - AUD + - AWG + - AZN + - BAM + - BBD + - BDT + - BGN + - BHD + - BIF + - BMD + - BND + - BOB + - BRL + - BSD + - BTN + - BWP + - BYN + - BZD + - CAD + - CDF + - CHF + - CLP + - CNY + - COP + - CRC + - CUP + - CVE + - CZK + - DJF + - DKK + - DOP + - DZD + - EGP + - ERN + - ETB + - EUR + - FJD + - FKP + - FOK + - GBP + - GEL + - GGP + - GHS + - GIP + - GMD + - GNF + - GTQ + - GYD + - HKD + - HNL + - HRK + - HTG + - HUF + - IDR + - ILS + - IMP + - INR + - IQD + - IRR + - ISK + - JEP + - JMD + - JOD + - JPY + - KES + - KGS + - KHR + - KID + - KMF + - KRW + - KWD + - KYD + - KZT + - LAK + - LBP + - LKR + - LRD + - LSL + - LYD + - MAD + - MDL + - MGA + - MKD + - MMK + - MNT + - MOP + - MRU + - MUR + - MVR + - MWK + - MXN + - MYR + - MZN + - NAD + - NGN + - NIO + - NOK + - NPR + - NZD + - OMR + - PAB + - PEN + - PGK + - PHP + - PKR + - PLN + - PYG + - QAR + - RON + - RSD + - RUB + - RWF + - SAR + - SBD + - SCR + - SDG + - SEK + - SGD + - SHP + - SLE + - SLL + - SOS + - SRD + - SSP + - STN + - SYP + - SZL + - THB + - TJS + - TMT + - TND + - TOP + - TRY + - TTD + - TVD + - TWD + - TZS + - UAH + - UGX + - USD + - UYU + - UZS + - VES + - VND + - VUV + - WST + - XAF + - XCD + - XDR + - XOF + - XPF + - YER + - ZAR + - ZMW + - ZWL + description: The currency of the vendor credit + exchange_rate: type: string - example: john.doe@example.com - description: The email of the user - nullable: true - disabled: - type: boolean - example: false - description: Whether the user is disabled + example: '1.2' nullable: true - access_role: + description: The exchange rate applied to the vendor credit + company_id: type: string - example: ADMIN - enum: - - SUPER_ADMIN - - ADMIN - - TEAM_MEMBER - - LIMITED_TEAM_MEMBER - - INTERVIEWER - description: The access role of the user + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - remote_created_at: - format: date-time - type: string - example: '2024-10-01T12:00:00Z' - description: The remote creation date of the user + description: The UUID of the associated company + tracking_categories: + example: + - 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true - remote_modified_at: - format: date-time + description: >- + The UUID of the tracking categories associated with the vendor + credit + type: array + items: + type: string + accounting_period_id: type: string - example: '2024-10-01T12:00:00Z' - description: The remote modification date of the user + example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f nullable: true + description: The UUID of the associated accounting period + line_items: + description: The line items associated with this vendor credit + type: array + items: + $ref: '#/components/schemas/LineItem' field_mappings: type: object example: - fav_dish: broccoli - fav_color: red + custom_field_1: value1 + custom_field_2: value2 + nullable: true description: >- The custom field mappings of the object between the remote 3rd party & Panora - nullable: true - additionalProperties: true id: type: string example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - description: The UUID of the user nullable: true + description: The UUID of the vendor credit record remote_id: type: string - example: id_1 - description: The remote ID of the user in the context of the 3rd Party - nullable: true - remote_data: - type: object - example: - fav_dish: broccoli - fav_color: red - description: The remote data of the user in the context of the 3rd Party + example: remote_id_1234 nullable: true - additionalProperties: true + description: The remote ID of the vendor credit created_at: format: date-time type: string - example: '2024-10-01T12:00:00Z' - description: The created date of the object - nullable: true + example: '2024-06-15T12:00:00Z' + nullable: false + description: The created date of the vendor credit modified_at: format: date-time type: string - example: '2024-10-01T12:00:00Z' - description: The modified date of the object - nullable: true - UnifiedAtsEeocsOutput: - type: object - properties: - candidate_id: - type: string - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - nullable: true - description: The UUID of the candidate - submitted_at: - type: string - example: '2024-10-01T12:00:00Z' + example: '2024-06-15T12:00:00Z' + nullable: false + description: The last modified date of the vendor credit + remote_updated_at: format: date-time - nullable: true - description: The submission date of the EEOC - race: - type: string - enum: - - AMERICAN_INDIAN_OR_ALASKAN_NATIVE - - ASIAN - - BLACK_OR_AFRICAN_AMERICAN - - HISPANIC_OR_LATINO - - WHITE - - NATIVE_HAWAIIAN_OR_OTHER_PACIFIC_ISLANDER - - TWO_OR_MORE_RACES - - DECLINE_TO_SELF_IDENTIFY - example: AMERICAN_INDIAN_OR_ALASKAN_NATIVE - nullable: true - description: The race of the candidate - gender: - type: string - example: MALE - enum: - - MALE - - FEMALE - - NON_BINARY - - OTHER - - DECLINE_TO_SELF_IDENTIFY - nullable: true - description: The gender of the candidate - veteran_status: - type: string - example: I_AM_NOT_A_PROTECTED_VETERAN - enum: - - I_AM_NOT_A_PROTECTED_VETERAN - - >- - I_IDENTIFY_AS_ONE_OR_MORE_OF_THE_CLASSIFICATIONS_OF_A_PROTECTED_VETERAN - - I_DONT_WISH_TO_ANSWER - nullable: true - description: The veteran status of the candidate - disability_status: type: string - enum: - - YES_I_HAVE_A_DISABILITY_OR_PREVIOUSLY_HAD_A_DISABILITY - - NO_I_DONT_HAVE_A_DISABILITY - - I_DONT_WISH_TO_ANSWER - example: YES_I_HAVE_A_DISABILITY_OR_PREVIOUSLY_HAD_A_DISABILITY - nullable: true - description: The disability status of the candidate - field_mappings: - type: object - example: - fav_dish: broccoli - fav_color: red - additionalProperties: true + example: '2024-06-15T12:00:00Z' nullable: true description: >- - The custom field mappings of the object between the remote 3rd party - & Panora - id: - type: string - example: 801f9ede-c698-4e66-a7fc-48d19eebaa4f - nullable: true - description: The UUID of the EEOC - remote_id: - type: string - example: id_1 - nullable: true - description: The remote ID of the EEOC in the context of the 3rd Party + The date when the vendor credit was last updated in the remote + system remote_data: type: object example: - fav_dish: broccoli - fav_color: red - nullable: true - additionalProperties: true - description: The remote data of the EEOC in the context of the 3rd Party - created_at: - format: date-time - type: string - example: '2024-10-01T12:00:00Z' + raw_data: + additional_field: some value nullable: true - description: The created date of the object - modified_at: - format: date-time - type: string - example: '2024-10-01T12:00:00Z' - nullable: true - description: The modified date of the object - UnifiedAccountingAccountOutput: - type: object - properties: {} - UnifiedAccountingAccountInput: - type: object - properties: {} - UnifiedAccountingAddressOutput: - type: object - properties: {} - UnifiedAccountingAttachmentOutput: - type: object - properties: {} - UnifiedAccountingAttachmentInput: - type: object - properties: {} - UnifiedAccountingBalancesheetOutput: - type: object - properties: {} - UnifiedAccountingCashflowstatementOutput: - type: object - properties: {} - UnifiedAccountingCompanyinfoOutput: - type: object - properties: {} - UnifiedAccountingContactOutput: - type: object - properties: {} - UnifiedAccountingContactInput: - type: object - properties: {} - UnifiedAccountingCreditnoteOutput: - type: object - properties: {} - UnifiedAccountingExpenseOutput: - type: object - properties: {} - UnifiedAccountingExpenseInput: - type: object - properties: {} - UnifiedAccountingIncomestatementOutput: - type: object - properties: {} - UnifiedAccountingInvoiceOutput: - type: object - properties: {} - UnifiedAccountingInvoiceInput: - type: object - properties: {} - UnifiedAccountingItemOutput: - type: object - properties: {} - UnifiedAccountingJournalentryOutput: - type: object - properties: {} - UnifiedAccountingJournalentryInput: - type: object - properties: {} - UnifiedAccountingPaymentOutput: - type: object - properties: {} - UnifiedAccountingPaymentInput: - type: object - properties: {} - UnifiedAccountingPhonenumberOutput: - type: object - properties: {} - UnifiedAccountingPurchaseorderOutput: - type: object - properties: {} - UnifiedAccountingPurchaseorderInput: - type: object - properties: {} - UnifiedAccountingTaxrateOutput: - type: object - properties: {} - UnifiedAccountingTrackingcategoryOutput: - type: object - properties: {} - UnifiedAccountingTransactionOutput: - type: object - properties: {} - UnifiedAccountingVendorcreditOutput: - type: object - properties: {} + description: The remote data of the vendor credit in the context of the 3rd Party UnifiedFilestorageDriveOutput: type: object properties: @@ -14101,7 +20902,7 @@ components: nullable: true field_mappings: type: object - example: &ref_143 + example: &ref_179 fav_dish: broccoli fav_color: red description: >- @@ -14187,7 +20988,7 @@ components: nullable: true field_mappings: type: object - example: *ref_143 + example: *ref_179 description: >- The custom field mappings of the object between the remote 3rd party & Panora @@ -14245,7 +21046,7 @@ components: description: The UUID of the permission tied to the folder field_mappings: type: object - example: &ref_144 + example: &ref_180 fav_dish: broccoli fav_color: red additionalProperties: true @@ -14336,7 +21137,7 @@ components: description: The UUID of the permission tied to the folder field_mappings: type: object - example: *ref_144 + example: *ref_180 additionalProperties: true nullable: true description: >- @@ -14501,13 +21302,13 @@ components: type: string example: ACTIVE nullable: true - enum: &ref_145 + enum: &ref_181 - ARCHIVED - ACTIVE - DRAFT description: The status of the product. Either ACTIVE, DRAFT OR ARCHIVED. images_urls: - example: &ref_146 + example: &ref_182 - https://myproduct/image nullable: true description: The URLs of the product images @@ -14525,7 +21326,7 @@ components: nullable: true description: The vendor of the product variants: - example: &ref_147 + example: &ref_183 - title: teeshirt price: 20 sku: '3' @@ -14537,7 +21338,7 @@ components: items: $ref: '#/components/schemas/Variant' tags: - example: &ref_148 + example: &ref_184 - tag_1 nullable: true description: The tags associated with the product @@ -14546,7 +21347,7 @@ components: type: string field_mappings: type: object - example: &ref_149 + example: &ref_185 fav_dish: broccoli fav_color: red nullable: true @@ -14597,10 +21398,10 @@ components: type: string example: ACTIVE nullable: true - enum: *ref_145 + enum: *ref_181 description: The status of the product. Either ACTIVE, DRAFT OR ARCHIVED. images_urls: - example: *ref_146 + example: *ref_182 nullable: true description: The URLs of the product images type: array @@ -14617,13 +21418,13 @@ components: nullable: true description: The vendor of the product variants: - example: *ref_147 + example: *ref_183 description: The variants of the product type: array items: $ref: '#/components/schemas/Variant' tags: - example: *ref_148 + example: *ref_184 nullable: true description: The tags associated with the product type: array @@ -14631,21 +21432,18 @@ components: type: string field_mappings: type: object - example: *ref_149 + example: *ref_185 nullable: true description: >- The custom field mappings of the object between the remote 3rd party & Panora - LineItem: - type: object - properties: {} UnifiedEcommerceOrderOutput: type: object properties: order_status: type: string example: UNSHIPPED - enum: &ref_150 + enum: &ref_186 - PENDING - UNSHIPPED - SHIPPED @@ -14660,7 +21458,7 @@ components: payment_status: type: string example: SUCCESS - enum: &ref_151 + enum: &ref_187 - SUCCESS - FAIL nullable: true @@ -14669,7 +21467,7 @@ components: type: string nullable: true example: AUD - enum: &ref_152 + enum: &ref_188 - AED - AFN - ALL @@ -14859,7 +21657,7 @@ components: type: string nullable: true example: PENDING - enum: &ref_153 + enum: &ref_189 - PENDING - FULFILLED - CANCELED @@ -14871,7 +21669,7 @@ components: description: The UUID of the customer associated with the order items: nullable: true - example: &ref_154 + example: &ref_190 - remote_id: '12345' product_id: prod_001 variant_id: var_001 @@ -14902,7 +21700,7 @@ components: $ref: '#/components/schemas/LineItem' field_mappings: type: object - example: &ref_155 + example: &ref_191 fav_dish: broccoli fav_color: red nullable: true @@ -14942,7 +21740,7 @@ components: order_status: type: string example: UNSHIPPED - enum: *ref_150 + enum: *ref_186 nullable: true description: The status of the order order_number: @@ -14953,14 +21751,14 @@ components: payment_status: type: string example: SUCCESS - enum: *ref_151 + enum: *ref_187 nullable: true description: The payment status of the order currency: type: string nullable: true example: AUD - enum: *ref_152 + enum: *ref_188 description: >- The currency of the order. Authorized value must be of type CurrencyCode (ISO 4217) @@ -14988,7 +21786,7 @@ components: type: string nullable: true example: PENDING - enum: *ref_153 + enum: *ref_189 description: The fulfillment status of the order customer_id: type: string @@ -14997,14 +21795,14 @@ components: description: The UUID of the customer associated with the order items: nullable: true - example: *ref_154 + example: *ref_190 description: The items in the order type: array items: $ref: '#/components/schemas/LineItem' field_mappings: type: object - example: *ref_155 + example: *ref_191 nullable: true description: >- The custom field mappings of the object between the remote 3rd party @@ -15181,7 +21979,7 @@ components: field_mappings: type: object nullable: true - example: &ref_156 + example: &ref_192 fav_dish: broccoli fav_color: red description: >- @@ -15253,7 +22051,7 @@ components: field_mappings: type: object nullable: true - example: *ref_156 + example: *ref_192 description: >- The custom field mappings of the attachment between the remote 3rd party & Panora diff --git a/packages/api/variables.MD b/packages/api/variables.MD index 8aaa1ec60..6ca21ab42 100644 --- a/packages/api/variables.MD +++ b/packages/api/variables.MD @@ -113,10 +113,10 @@ | MAILCHIMP_MARKETINGAUTOMATION_CLOUD_CLIENT_SECRET | | | | KLAVIYO_TICKETING_CLOUD_CLIENT_ID | | | | KLAVIYO_TICKETING_CLOUD_CLIENT_SECRET | | | -| NOTION_MANAGEMENT_CLOUD_CLIENT_ID | | | -| NOTION_MANAGEMENT_CLOUD_CLIENT_SECRET | | | -| SLACK_MANAGEMENT_CLOUD_CLIENT_ID | | | -| SLACK_MANAGEMENT_CLOUD_CLIENT_SECRET | | | +| NOTION_PRODUCTIVITY_CLOUD_CLIENT_ID | | | +| NOTION_PRODUCTIVITY_CLOUD_CLIENT_SECRET | | | +| SLACK_PRODUCTIVITY_CLOUD_CLIENT_ID | | | +| SLACK_PRODUCTIVITY_CLOUD_CLIENT_SECRET | | | | GREENHOUSE_ATS_CLOUD_CLIENT_ID | | | | GREENHOUSE_ATS_CLOUD_CLIENT_SECRET | | | | JOBADDER_ATS_CLOUD_CLIENT_ID | | | diff --git a/packages/shared/src/authUrl.ts b/packages/shared/src/authUrl.ts index 2368930fc..6b7d66e72 100644 --- a/packages/shared/src/authUrl.ts +++ b/packages/shared/src/authUrl.ts @@ -1,8 +1,19 @@ +import * as crypto from 'crypto'; import { CONNECTORS_METADATA } from './connectors/metadata'; -import { needsEndUserSubdomain, needsScope, needsSubdomain, OAuth2AuthData, providerToType } from './envConfig'; -import { AuthStrategy, DynamicAuthorization, ProviderConfig, StringAuthorization } from './types'; +import { + needsEndUserSubdomain, + needsScope, + needsSubdomain, + OAuth2AuthData, + providerToType +} from './envConfig'; +import { + AuthStrategy, + DynamicAuthorization, + ProviderConfig, + StringAuthorization +} from './types'; import { randomString } from './utils'; -import * as crypto from 'crypto'; interface AuthParams { projectId: string; @@ -16,80 +27,75 @@ interface AuthParams { value: string | null; }; additionalParams?: { - end_user_domain: string; // needed for instance with shopify or sharepoint to construct the auth domain + end_user_domain: string; } } function generateCodes() { const base64URLEncode = (str: Buffer): string => { - return str.toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, ''); + return str.toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); } const verifier = base64URLEncode(crypto.randomBytes(32)); + const challenge = base64URLEncode( + crypto.createHash('sha256').update(Buffer.from(verifier)).digest() + ); - const sha256 = (buffer: Buffer): Buffer => { - return crypto.createHash('sha256').update(buffer).digest(); - } - - const challenge = base64URLEncode(sha256(Buffer.from(verifier))); - - return { - codeVerifier: verifier, - codeChallenge: challenge - } + return { codeVerifier: verifier, codeChallenge: challenge }; } -// make sure to check wether its api_key or oauth2 to build the right auth -// make sure to check if client has own credentials to connect or panora managed ones -export const constructAuthUrl = async ({ projectId, linkedUserId, providerName, returnUrl, apiUrl, vertical, additionalParams, redirectUriIngress }: AuthParams) => { +export const constructAuthUrl = async ({ + projectId, + linkedUserId, + providerName, + returnUrl, + apiUrl, + vertical, + additionalParams, + redirectUriIngress +}: AuthParams) => { const config = CONNECTORS_METADATA[vertical.toLowerCase()][providerName]; if (!config) { throw new Error(`Unsupported provider: ${providerName}`); } + let baseRedirectURL = apiUrl; - // We check if https is needed in local if yes we take the ingress setup in .env and passed through redirectUriIngress - if (config.options && config.options.local_redirect_uri_in_https === true && redirectUriIngress && redirectUriIngress.status === true) { + if (config.options?.local_redirect_uri_in_https && redirectUriIngress?.status) { baseRedirectURL = redirectUriIngress.value!; } - let encodedRedirectUrl = encodeURIComponent(`${baseRedirectURL}/connections/oauth/callback`); + + const encodedRedirectUrl = encodeURIComponent(`${baseRedirectURL}/connections/oauth/callback`); let state = encodeURIComponent(JSON.stringify({ projectId, linkedUserId, providerName, vertical, returnUrl })); + if (providerName === 'microsoftdynamicssales') { - state = encodeURIComponent(JSON.stringify({ projectId, linkedUserId, providerName, vertical, returnUrl, resource: additionalParams!.end_user_domain })); + state = encodeURIComponent(JSON.stringify({ + projectId, linkedUserId, providerName, vertical, returnUrl, + resource: additionalParams!.end_user_domain + })); } - if(providerName === 'squarespace'){ + + if (['deel', 'squarespace'].includes(providerName)) { const randomState = randomString(); - state = encodeURIComponent(randomState + 'squarespace_delimiter' + Buffer.from(JSON.stringify({ - projectId, - linkedUserId, - providerName, - vertical, - returnUrl, + state = encodeURIComponent(randomState + `${providerName}_delimiter` + Buffer.from(JSON.stringify({ + projectId, linkedUserId, providerName, vertical, returnUrl, resource: additionalParams!.end_user_domain! - })).toString('base64')); + })).toString('base64')); } - // console.log('State : ', JSON.stringify({ projectId, linkedUserId, providerName, vertical, returnUrl })); - // console.log('encodedRedirect URL : ', encodedRedirectUrl); - // const vertical = findConnectorCategory(providerName); - if (vertical === null) { + + if (vertical === null) { throw new ReferenceError('vertical is null'); } + const authStrategy = config.authStrategy!.strategy; switch (authStrategy) { case AuthStrategy.oauth2: return handleOAuth2Url({ - providerName, - vertical, - authStrategy, - projectId, - config, - encodedRedirectUrl, - state, - apiUrl, - additionalParams + providerName, vertical, authStrategy, projectId, config, + encodedRedirectUrl, state, apiUrl, additionalParams }); case AuthStrategy.api_key: return handleApiKeyUrl(); @@ -108,111 +114,93 @@ interface HandleOAuth2Url { state: string; apiUrl: string; additionalParams?: { - end_user_domain: string; // needed for instance with shopify or sharepoint to construct the auth domain + end_user_domain: string; } } -const handleOAuth2Url = async (input: HandleOAuth2Url) => { - const { - providerName, - vertical, - authStrategy, - projectId, - config, - encodedRedirectUrl, - state, - apiUrl , - additionalParams, - } = input; - - const type = providerToType(providerName, vertical, authStrategy); +const handleOAuth2Url = async ({ + providerName, + vertical, + authStrategy, + projectId, + config, + encodedRedirectUrl, + state, + apiUrl, + additionalParams, +}: HandleOAuth2Url) => { + const type = providerToType(providerName, vertical, authStrategy); - // 1. env if selfhost and no custom - // 2. backend if custom credentials - // same for authBaseUrl with subdomain - const DATA = await fetch(`${apiUrl}/connection_strategies/getCredentials?projectId=${projectId}&type=${type}`); - const data = await DATA.json() as OAuth2AuthData; - - // console.log("Fetched Data ", JSON.stringify(data)) + const response = await fetch(`${apiUrl}/connection_strategies/getCredentials?projectId=${projectId}&type=${type}`); + const data = await response.json() as OAuth2AuthData; const clientId = data.CLIENT_ID; - if (!clientId) throw new ReferenceError(`No client id for type ${type}`) + if (!clientId) throw new ReferenceError(`No client id for type ${type}`); + const scopes = data.SCOPE; - - const { urls: urls } = config; - const { authBaseUrl: baseUrl } = urls; - - if (!baseUrl) throw new ReferenceError(`No authBaseUrl found for type ${type}`) + const { urls: { authBaseUrl: baseUrl } } = config; + if (!baseUrl) throw new ReferenceError(`No authBaseUrl found for type ${type}`); let BASE_URL: string; - // construct the baseAuthUrl based on the fact that client may use custom subdomain - if( needsSubdomain(providerName, vertical) ) { - if (typeof baseUrl === 'string') { - BASE_URL = baseUrl; - } else { - BASE_URL = (baseUrl as DynamicAuthorization)(data.SUBDOMAIN as string); - } + if (needsSubdomain(providerName, vertical)) { + BASE_URL = typeof baseUrl === 'string' ? baseUrl : (baseUrl as DynamicAuthorization)(data.SUBDOMAIN as string); } else if (needsEndUserSubdomain(providerName, vertical)) { - if (typeof baseUrl === 'string') { - BASE_URL = baseUrl; - } else { - BASE_URL = (baseUrl as DynamicAuthorization)(additionalParams!.end_user_domain); - } + BASE_URL = typeof baseUrl === 'string' ? baseUrl : (baseUrl as DynamicAuthorization)(additionalParams!.end_user_domain); } else { BASE_URL = baseUrl as StringAuthorization; } - // console.log('BASE URL IS '+ BASE_URL) - if (!baseUrl || !BASE_URL) { + if (!BASE_URL) { throw new Error(`Unsupported provider: ${providerName}`); } - // Default URL structure let params = `response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodedRedirectUrl}&state=${state}`; - if (providerName === 'helpscout') { - params = `client_id=${encodeURIComponent(clientId)}&state=${state}`; - } - if (providerName === 'pipedrive' || providerName === 'shopify' || providerName === 'squarespace') { - params = `client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodedRedirectUrl}&state=${state}`; - } - if (providerName === 'faire') { - params = `applicationId=${encodeURIComponent(clientId)}&redirectUrl=${encodedRedirectUrl}&state=${state}`; - } - if (providerName === 'ebay') { - params = `response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${data.RUVALUE}&state=${state}`; - } - if (providerName === 'amazon') { - params = `application_id=${encodeURIComponent(data.APPLICATION_ID)}&state=${state}&version=beta`; + // Provider-specific parameter adjustments + switch (providerName) { + case 'helpscout': + params = `client_id=${encodeURIComponent(clientId)}&state=${state}`; + break; + case 'pipedrive': + case 'shopify': + case 'squarespace': + params = `client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodedRedirectUrl}&state=${state}`; + break; + case 'faire': + params = `applicationId=${encodeURIComponent(clientId)}&redirectUrl=${encodedRedirectUrl}&state=${state}`; + break; + case 'ebay': + params = `response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${data.RUVALUE}&state=${state}`; + break; + case 'amazon': + params = `application_id=${encodeURIComponent(data.APPLICATION_ID)}&state=${state}&version=beta`; + break; } + // Handle scopes if (needsScope(providerName, vertical) && scopes) { if (providerName === 'slack') { params += `&scope=&user_scope=${encodeURIComponent(scopes)}`; } else if (providerName === 'microsoftdynamicssales') { const url = new URL(BASE_URL); - // Extract the base URL without parameters - const base = url.origin + url.pathname; - // Extract the resource parameter + BASE_URL = url.origin + url.pathname; const resource = url.searchParams.get('resource'); - BASE_URL = base; - let b = `https://${resource}/.default`; - b += (' offline_access'); - params += `&scope=${encodeURIComponent(b)}`; + const scopeValue = `https://${resource}/.default offline_access`; + params += `&scope=${encodeURIComponent(scopeValue)}`; + } else if (providerName === 'deel') { + params += `&scope=${encodeURIComponent(scopes.replace(/\t/g, ' '))}`; } else { params += `&scope=${encodeURIComponent(scopes)}`; } } - // Special cases for certain providers + // Additional provider-specific parameters switch (providerName) { case 'zoho': case 'squarespace': params += '&access_type=offline'; break; case 'jira': - params = `audience=api.atlassian.com&${params}&prompt=consent`; - break; case 'jira_service_mgmt': params = `audience=api.atlassian.com&${params}&prompt=consent`; break; @@ -220,40 +208,38 @@ const handleOAuth2Url = async (input: HandleOAuth2Url) => { params += '&code_challenge=&code_challenge_method='; break; case 'gorgias': - params = `&nonce=${randomString()}`; + params += `&nonce=${randomString()}`; break; case 'googledrive': - params = `${params}&access_type=offline`; + params += '&access_type=offline'; break; case 'dropbox': - params = `${params}&token_access_type=offline` + params += '&token_access_type=offline'; break; case 'basecamp': - params += `&type=web_server` + params += '&type=web_server'; break; case 'lever': - params += `&audience=https://api.lever.co/v1/` + params += '&audience=https://api.lever.co/v1/'; break; case 'notion': - params += `&owner=user` + params += '&owner=user'; break; case 'klaviyo': - const {codeChallenge, codeVerifier}= generateCodes() - params += `&code_challenge_method=S256&code_challenge=${codeChallenge}` // todo: store codeVerifier in a store - break; - default: + const { codeChallenge, codeVerifier } = generateCodes(); + params += `&code_challenge_method=S256&code_challenge=${codeChallenge}`; break; } - const finalAuthUrl = `${BASE_URL}?${params}`; - // console.log('Final Authentication : ', finalAuthUrl); - return finalAuthUrl; + return `${BASE_URL}?${params}`; } const handleApiKeyUrl = async () => { + // Placeholder for API key handling return; } const handleBasicUrl = async () => { + // Placeholder for basic auth handling return; } diff --git a/packages/shared/src/categories.ts b/packages/shared/src/categories.ts index 67630071e..5cb99075e 100644 --- a/packages/shared/src/categories.ts +++ b/packages/shared/src/categories.ts @@ -6,7 +6,7 @@ export enum ConnectorCategory { Ticketing = 'ticketing', MarketingAutomation = 'marketingautomation', FileStorage = 'filestorage', - Management = 'management', + Productivity = 'productivity', Ecommerce = 'ecommerce' } diff --git a/packages/shared/src/connectors/enum.ts b/packages/shared/src/connectors/enum.ts index 490be976d..78c53bba0 100644 --- a/packages/shared/src/connectors/enum.ts +++ b/packages/shared/src/connectors/enum.ts @@ -9,22 +9,20 @@ export enum CrmConnectors { export enum EcommerceConnectors { SHOPIFY = 'shopify', + WOOCOMMERCE = 'woocommerce', + SQUARESPACE = 'squarespace', + AMAZON = 'amazon' } export enum TicketingConnectors { ZENDESK = 'zendesk', FRONT = 'front', JIRA = 'jira', - GORGIAS = 'gorgias', GITHUB = 'github', GITLAB = 'gitlab', LINEAR = 'linear' } -export enum AccountingConnectors { - PENNYLANE = 'pennylane', - FRESHBOOKS = 'freshbooks', - CLEARBOOKS = 'clearbooks', - FREEAGENT = 'freeagent', - SAGE = 'sage', +export enum FilestorageConnectors { + BOX = 'box' } diff --git a/packages/shared/src/connectors/index.ts b/packages/shared/src/connectors/index.ts index aadbb1035..b16d04442 100644 --- a/packages/shared/src/connectors/index.ts +++ b/packages/shared/src/connectors/index.ts @@ -1,8 +1,8 @@ export const CRM_PROVIDERS = ['zoho', 'zendesk', 'hubspot', 'pipedrive', 'attio', 'close']; export const HRIS_PROVIDERS = []; -export const ATS_PROVIDERS = []; +export const ATS_PROVIDERS = ['ashby']; export const ACCOUNTING_PROVIDERS = []; export const TICKETING_PROVIDERS = ['zendesk', 'front', 'jira', 'gorgias', 'gitlab', 'github', 'linear']; export const MARKETINGAUTOMATION_PROVIDERS = []; -export const FILESTORAGE_PROVIDERS = []; -export const ECOMMERCE_PROVIDERS = ['shopify']; +export const FILESTORAGE_PROVIDERS = ['box']; +export const ECOMMERCE_PROVIDERS = ['shopify', 'woocommerce', 'squarespace', 'amazon']; diff --git a/packages/shared/src/connectors/metadata.ts b/packages/shared/src/connectors/metadata.ts index d7bda817f..9f0368312 100644 --- a/packages/shared/src/connectors/metadata.ts +++ b/packages/shared/src/connectors/metadata.ts @@ -131,6 +131,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { logoPath: 'https://media.licdn.com/dms/image/C4D0BAQFOaK6KXEYj_w/company-logo_200_200/0/1630489791871/project_affinity_logo?e=2147483647&v=beta&t=u8j-1u3nO2m6vqgT170WJMCJyFSDiLYS_VguYOllNMI', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, + primaryColor: '#244CED', authStrategy: { strategy: AuthStrategy.basic, properties: ['password'] @@ -496,7 +497,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { }, logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRqz0aID6B-InxK_03P7tCtqpXNXdawBcro67CyEE0I5g&s', description: 'Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users', - active: false, + active: true, authStrategy: { strategy: AuthStrategy.oauth2 } @@ -580,6 +581,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTOxBDw6TkTaxR4EUGI_lNBLl4BCpd3AzXnr30cU_VEaB0jHFh__fFZJHXPB1t-451Eno8&usqp=CAU', description: 'Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users', active: false, + primaryColor: '#5644D8', authStrategy: { strategy: AuthStrategy.api_key, properties: ['api_key'] @@ -1210,6 +1212,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { logoPath: 'https://sbp-plugin-images.s3.eu-west-1.amazonaws.com/technologies526_65670ec92e038_brevo300.jpg', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, + primaryColor: '#0B996F', authStrategy: { strategy: AuthStrategy.api_key, properties: ['api_key'] @@ -1238,6 +1241,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { logoPath: 'https://images.ctfassets.net/p03bi75xct27/2tVvkghDdMJxzkMca2QLnr/31b520c5e07db0103948af171fb54e99/ashby_logo_square.jpeg?q=80&fm=webp&w=2048', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, + primaryColor: '#4a3ead', authStrategy: { strategy: AuthStrategy.basic, properties: ['username'] @@ -1251,7 +1255,8 @@ export const CONNECTORS_METADATA: ProvidersConfig = { }, logoPath: 'https://play-lh.googleusercontent.com/c4BW9wr_QAiIeVBYHhP7rs06w99xJzxgLvmL5I1mkucC3_ATMyL1t7Doz0_LQ0X-qS0', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', - active: false, + active: true, + primaryColor: '#599D16', authStrategy: { strategy: AuthStrategy.basic, properties: ['username', 'company_subdomain'] @@ -2087,12 +2092,12 @@ export const CONNECTORS_METADATA: ProvidersConfig = { 'gusto': { urls: { docsUrl: 'https://docs.gusto.com/app-integrations/docs/introduction', - apiUrl: 'https://api.gusto.com/v1', + apiUrl: 'https://api.gusto.com', // api.gusto-demo.com authBaseUrl: 'https://api.gusto-demo.com/oauth/authorize' }, logoPath: 'https://cdn.runalloy.com/landing/uploads-new/Gusto_Logo_67ca008403.png', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', - active: false, + active: true, authStrategy: { strategy: AuthStrategy.oauth2 } @@ -2761,7 +2766,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { } }, }, - 'management': { + 'productivity': { 'notion': { urls: { docsUrl: 'https://developers.notion.com/docs/getting-started', @@ -2802,6 +2807,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTH_-bQ399xl-yfJYhbLraU-w0yWBcppLf8NA&s', description: 'Sync & Create orders, fulfillments, fulfillment orders, customers and products', active: false, + primaryColor: '#000001', authStrategy: { strategy: AuthStrategy.api_key, properties: ['api_key', 'store_hash'] @@ -2862,9 +2868,10 @@ export const CONNECTORS_METADATA: ProvidersConfig = { docsUrl: 'https://shopify.dev/docs/apps/build', apiUrl: (storeName: string) => `https://${storeName}.myshopify.com`, }, - logoPath: 'https://cdn.eastsideco.com/media/v3/services/ecommerce-services/shopify-logo.png', + logoPath: 'https://www.pngall.com/wp-content/uploads/13/Shopify-Logo-PNG.png', description: 'Sync & Create orders, fulfillments, fulfillment orders, customers and products', active: true, + primaryColor: '#5E8E3E', authStrategy: { strategy: AuthStrategy.api_key, properties: ['api_key', 'store_url'] @@ -2936,6 +2943,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSHiusc7S5-BoiU1YKCztJMv_Qj7wlim4TwbA&s', description: 'Sync & Create orders, fulfillments, fulfillment orders, customers and products', active: true, + primaryColor: '#702963', authStrategy: { strategy: AuthStrategy.basic, properties: ['username', 'password', 'store_url'] diff --git a/packages/shared/src/standardObjects.ts b/packages/shared/src/standardObjects.ts index 0c3446222..153d47cb5 100644 --- a/packages/shared/src/standardObjects.ts +++ b/packages/shared/src/standardObjects.ts @@ -2,52 +2,103 @@ export enum CrmObject { company = 'company', contact = 'contact', deal = 'deal', - event = 'event', lead = 'lead', note = 'note', task = 'task', + engagement = 'engagement', + stage = 'stage', user = 'user', } -export enum HrisObject {} +export enum HrisObject { + bankinfo = 'bankinfo', + benefit = 'benefit', + company = 'company', + dependent = 'dependent', + employee = 'employee', + employeepayrollrun = 'employeepayrollrun', + employerbenefit = 'employerbenefit', + employment = 'employment', + group = 'group', + location = 'location', + paygroup = 'paygroup', + payrollrun = 'payrollrun', + timeoff = 'timeoff', + timeoffbalance = 'timeoffbalance', + timesheetentry = 'timesheetentry', +} export enum AtsObject { - activity = 'activity', - application = 'application', - attachment = 'attachment', - candidate = 'candidate', - department = 'department', - eeocs = 'eeocs', - interview = 'interview', - job = 'job', - jobinterviewstage = 'jobinterviewstage', - offer = 'offer', - office = 'office', - rejectreason = 'rejectreason', - scorecard = 'scorecard', - tag = 'tag', - user = 'user' + activity = 'activity', + application = 'application', + attachment = 'attachment', + candidate = 'candidate', + department = 'department', + interview = 'interview', + jobinterviewstage = 'jobinterviewstage', + job = 'job', + offer = 'offer', + office = 'office', + rejectreason = 'rejectreason', + scorecard = 'scorecard', + tag = 'tag', + user = 'user', + eeocs = 'eeocs', } -export enum AccountingObject {} +export enum AccountingObject { + balancesheet = 'balancesheet', + cashflowstatement = 'cashflowstatement', + companyinfo = 'companyinfo', + contact = 'contact', + creditnote = 'creditnote', + expense = 'expense', + incomestatement = 'incomestatement', + invoice = 'invoice', + item = 'item', + journalentry = 'journalentry', + payment = 'payment', + phonenumber = 'phonenumber', + purchaseorder = 'purchaseorder', + taxrate = 'taxrate', + trackingcategory = 'trackingcategory', + transaction = 'transaction', + vendorcredit = 'vendorcredit', + account = 'account', + address = 'address', + attachment = 'attachment', +} export enum EcommerceObject { - order = 'order', - fulfillment = 'fulfillment', - product = 'product', - customer = 'customer', - fulfillmentorders = 'fulfillmentorders' + order = 'order', + fulfillment = 'fulfillment', + product = 'product', + customer = 'customer', + fulfillmentorders = 'fulfillmentorders' } export enum FileStorageObject { - drive = 'drive', file = 'file', folder = 'folder', + permission = 'permission', + drive = 'drive', + sharedlink = 'sharedlink', group = 'group', - user = 'user' + user = 'user', } -export enum MarketingAutomationObject {} +export enum MarketingAutomationObject { + action = 'action', + automation = 'automation', + campaign = 'campaign', + contact = 'contact', + email = 'email', + event = 'event', + list = 'list', + message = 'message', + template = 'template', + user = 'user', +} export enum TicketingObject { ticket = 'ticket', @@ -58,7 +109,7 @@ export enum TicketingObject { account = 'account', tag = 'tag', team = 'team', - collection = 'collection' + collection = 'collection', } // Utility function to prepend prefix to enum values // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 3115784e6..22c3453b1 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -30,6 +30,7 @@ export type ProviderConfig = { active?: boolean; customPropertiesUrl?: string; authStrategy: AuthType; + primaryColor?: string; urls: { docsUrl: string; apiUrl: StaticApiUrl | DynamicApiUrl;