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/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/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 69bc42478..ead5ee43c 100644 --- a/packages/api/scripts/init.sql +++ b/packages/api/scripts/init.sql @@ -178,7 +178,6 @@ CREATE TABLE managed_webhooks 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 ( @@ -200,11 +199,9 @@ CREATE TABLE hris_time_off 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 ( @@ -223,7 +220,6 @@ CREATE TABLE hris_payroll_runs CONSTRAINT PK_hris_payroll_runs PRIMARY KEY ( id_hris_payroll_run ) ); - -- ************************************** hris_pay_groups CREATE TABLE hris_pay_groups ( @@ -238,7 +234,6 @@ CREATE TABLE hris_pay_groups CONSTRAINT PK_hris_pay_groups PRIMARY KEY ( id_hris_pay_group ) ); - -- ************************************** hris_locations CREATE TABLE hris_locations ( @@ -249,24 +244,20 @@ CREATE TABLE hris_locations 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 NOT 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, - id_hris_employee uuid NULL, - id_hris_company uuid NULL, CONSTRAINT PK_hris_locations PRIMARY KEY ( id_hris_location ) ); - COMMENT ON COLUMN hris_locations.location_type IS 'HOME, WORK'; -COMMENT ON COLUMN hris_locations.id_hris_employee IS 'uuid of the employee this location belongs to'; -COMMENT ON COLUMN hris_locations.id_hris_company IS 'uuid of the company this location belongs to'; - -- ************************************** hris_groups CREATE TABLE hris_groups @@ -283,14 +274,13 @@ CREATE TABLE hris_groups 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, @@ -300,11 +290,9 @@ CREATE TABLE hris_employer_benefits remote_was_deleted boolean 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_hris_employer_benefits PRIMARY KEY ( id_hris_employer_benefit ) ); - -- ************************************** hris_companies CREATE TABLE hris_companies ( @@ -318,11 +306,9 @@ CREATE TABLE hris_companies modified_at timestamp with time zone NOT NULL, remote_was_deleted boolean NOT NULL, id_connection uuid NOT NULL, - location uuid NULL, CONSTRAINT PK_hris_companies PRIMARY KEY ( id_hris_company ) ); - -- ************************************** fs_users CREATE TABLE fs_users ( @@ -563,6 +549,7 @@ CREATE TABLE connector_sets ); + -- ************************************** connection_strategies CREATE TABLE connection_strategies ( @@ -1066,11 +1053,17 @@ 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, @@ -1092,27 +1085,17 @@ CREATE TABLE hris_employees employment_status text NULL, termination_date date NULL, avatar_url 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, - manager uuid 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 ( @@ -1823,7 +1806,6 @@ CREATE INDEX FK_proectID_linked_users ON linked_users 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 ( @@ -1841,13 +1823,11 @@ CREATE TABLE hris_timesheet_entries 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 ( @@ -1865,13 +1845,11 @@ CREATE TABLE hris_time_off_balances 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 ( @@ -1896,21 +1874,17 @@ CREATE TABLE hris_employments 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 ( @@ -1932,22 +1906,19 @@ CREATE TABLE hris_employee_payroll_runs 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_dependent uuid NOT NULL, + id_hris_dependents uuid NOT NULL, first_name text NULL, last_name text NULL, middle_name text NULL, @@ -1965,22 +1936,25 @@ CREATE TABLE hris_dependents 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_dependent ), + 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, @@ -1988,28 +1962,19 @@ CREATE TABLE hris_benefits start_date timestamp with time zone NULL, end_date timestamp with time zone NULL, id_hris_employer_benefit 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_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 ( @@ -2028,13 +1993,11 @@ CREATE TABLE hris_bank_infos 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 ( @@ -2378,18 +2341,15 @@ CREATE TABLE api_keys 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 ( @@ -2423,6 +2383,7 @@ 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, @@ -2621,6 +2582,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, @@ -2630,7 +2592,6 @@ CREATE TABLE acc_addresses id_acc_contact uuid NULL, id_acc_company_info uuid NULL, id_connection uuid NOT NULL, - remote_id text NULL, CONSTRAINT PK_acc_addresses PRIMARY KEY ( id_acc_address ) ); @@ -2706,7 +2667,6 @@ CREATE INDEX FK_invite_link_linkedUserID ON invite_links id_linked_user ); - -- ************************************** hris_employee_payroll_runs_taxes CREATE TABLE hris_employee_payroll_runs_taxes ( @@ -2721,13 +2681,11 @@ CREATE TABLE hris_employee_payroll_runs_taxes 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 ( @@ -2741,34 +2699,30 @@ CREATE TABLE hris_employee_payroll_runs_earnings 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, - 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_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 ( @@ -2956,6 +2910,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, 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..7463df68c 100644 --- a/packages/api/src/@core/connections/connections.controller.ts +++ b/packages/api/src/@core/connections/connections.controller.ts @@ -95,6 +95,14 @@ export class ConnectionsController { // Step 3: Parse the JSON stateData = JSON.parse(decodedState); + } else if (state.includes('deel_delimiter')) { + // squarespace asks for a random alphanumeric value + // Split the random part and the base64 part + const [randomPart, base64Part] = + decodeURIComponent(state).split('deel_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 (state.includes('squarespace_delimiter')) { // squarespace asks for a random alphanumeric value // Split the random part and the base64 part 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/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..cf809851d 100644 --- a/packages/shared/src/authUrl.ts +++ b/packages/shared/src/authUrl.ts @@ -54,12 +54,23 @@ export const constructAuthUrl = async ({ projectId, linkedUserId, providerName, if (config.options && config.options.local_redirect_uri_in_https === true && redirectUriIngress && redirectUriIngress.status === true) { 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 })); } - if(providerName === 'squarespace'){ + if (providerName === 'deel') { + const randomState = randomString(); + state = encodeURIComponent(randomState + 'deel_delimiter' + Buffer.from(JSON.stringify({ + projectId, + linkedUserId, + providerName, + vertical, + returnUrl, + resource: additionalParams!.end_user_domain! + })).toString('base64')); + } + if (providerName === 'squarespace') { const randomState = randomString(); state = encodeURIComponent(randomState + 'squarespace_delimiter' + Buffer.from(JSON.stringify({ projectId, @@ -68,7 +79,7 @@ export const constructAuthUrl = async ({ 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); @@ -199,6 +210,8 @@ const handleOAuth2Url = async (input: HandleOAuth2Url) => { let b = `https://${resource}/.default`; b += (' offline_access'); params += `&scope=${encodeURIComponent(b)}`; + } else if (providerName === 'deel') { + params += `&scope=${encodeURIComponent(scopes.replace(/\t/g, ' '))}`; } else { params += `&scope=${encodeURIComponent(scopes)}`; } 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 c9a98c1fb..a66547391 100644 --- a/packages/shared/src/connectors/enum.ts +++ b/packages/shared/src/connectors/enum.ts @@ -9,21 +9,19 @@ 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' } -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 c7e99e5ea..105ca557a 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']; +export const TICKETING_PROVIDERS = ['zendesk', 'front', 'jira', 'gitlab', 'github']; 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..74656d7fb 100644 --- a/packages/shared/src/connectors/metadata.ts +++ b/packages/shared/src/connectors/metadata.ts @@ -2087,12 +2087,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 +2761,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { } }, }, - 'management': { + 'productivity': { 'notion': { urls: { docsUrl: 'https://developers.notion.com/docs/getting-started', 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