From 0309c1e6e60e9df1fa6b33b380390d478e615f5d Mon Sep 17 00:00:00 2001 From: nael Date: Thu, 8 Aug 2024 20:45:38 +0200 Subject: [PATCH 1/2] :sparkles: Bamboohr + woocomerce integration --- apps/magic-link/src/lib/ProviderModal.tsx | 121 +---------- packages/api/prisma/schema.prisma | 19 +- .../api/src/@core/connections/@utils/types.ts | 5 +- .../ats/services/ashby/ashby.service.ts | 6 +- .../ats/services/bamboohr/bamboohr.service.ts | 74 ++----- .../ats/services/workday/workday.service.ts | 6 +- .../connections/connections.controller.ts | 20 +- .../crm/services/affinity/affinity.service.ts | 6 +- .../ecommerce/ecommerce.connection.module.ts | 2 + .../services/shopify/shopify.service.ts | 45 +---- .../woocommerce/woocommerce.service.ts | 155 ++++++++++++++ .../services/brevo/brevo.service.ts | 4 +- .../ticketing/services/dixa/dixa.service.ts | 6 +- .../api/src/@core/utils/dtos/query.dto.ts | 7 +- .../types/original/original.ecommerce.ts | 29 ++- .../ecommerce/customer/customer.controller.ts | 3 + .../src/ecommerce/customer/customer.module.ts | 11 +- .../customer/services/woocommerce/index.ts | 66 ++++++ .../customer/services/woocommerce/mappers.ts | 184 +++++++++++++++++ .../customer/services/woocommerce/types.ts | 53 +++++ .../ecommerce/customer/sync/sync.service.ts | 19 +- .../fulfillment/fulfillment.controller.ts | 3 + .../fulfillmentorders.controller.ts | 1 + .../src/ecommerce/order/order.controller.ts | 3 + .../api/src/ecommerce/order/order.module.ts | 9 +- .../order/services/woocommerce/index.ts | 107 ++++++++++ .../order/services/woocommerce/mappers.ts | 190 ++++++++++++++++++ .../order/services/woocommerce/types.ts | 148 ++++++++++++++ .../src/ecommerce/order/sync/sync.service.ts | 1 + .../api/src/ecommerce/order/types/index.ts | 5 +- .../ecommerce/product/product.controller.ts | 5 +- .../src/ecommerce/product/product.module.ts | 4 + .../product/services/product.service.ts | 121 +++++------ .../product/services/woocommerce/index.ts | 110 ++++++++++ .../product/services/woocommerce/mappers.ts | 148 ++++++++++++++ .../product/services/woocommerce/types.ts | 109 ++++++++++ .../ecommerce/product/sync/sync.service.ts | 3 + .../api/src/ecommerce/product/types/index.ts | 5 +- .../ticketing/account/sync/sync.service.ts | 16 +- packages/api/swagger/swagger-spec.yaml | 116 +---------- packages/shared/src/connectors/metadata.ts | 43 ++-- 41 files changed, 1514 insertions(+), 474 deletions(-) create mode 100644 packages/api/src/@core/connections/ecommerce/services/woocommerce/woocommerce.service.ts create mode 100644 packages/api/src/ecommerce/customer/services/woocommerce/index.ts create mode 100644 packages/api/src/ecommerce/customer/services/woocommerce/mappers.ts create mode 100644 packages/api/src/ecommerce/customer/services/woocommerce/types.ts create mode 100644 packages/api/src/ecommerce/order/services/woocommerce/index.ts create mode 100644 packages/api/src/ecommerce/order/services/woocommerce/mappers.ts create mode 100644 packages/api/src/ecommerce/order/services/woocommerce/types.ts create mode 100644 packages/api/src/ecommerce/product/services/woocommerce/index.ts create mode 100644 packages/api/src/ecommerce/product/services/woocommerce/mappers.ts create mode 100644 packages/api/src/ecommerce/product/services/woocommerce/types.ts diff --git a/apps/magic-link/src/lib/ProviderModal.tsx b/apps/magic-link/src/lib/ProviderModal.tsx index c4f857f03..ecbd85d9d 100644 --- a/apps/magic-link/src/lib/ProviderModal.tsx +++ b/apps/magic-link/src/lib/ProviderModal.tsx @@ -27,10 +27,6 @@ import { } from "@/components/ui/dialog" import useCreateApiKeyConnection from '@/hooks/queries/useCreateApiKeyConnection'; -interface IApiKeyFormData { - apikey: string; - [key : string]: string -} interface IBasicAuthFormData { [key : string]: string @@ -48,7 +44,6 @@ const ProviderModal = () => { }>({provider:'',category:''}); const [startFlow, setStartFlow] = useState(false); const [preStartFlow, setPreStartFlow] = useState(false); - const [openApiKeyDialog,setOpenApiKeyDialog] = useState(false); const [openBasicAuthDialog,setOpenBasicAuthDialog] = useState(false); const [openDomainDialog, setOpenDomainDialog] = useState(false); const [projectId, setProjectId] = useState(""); @@ -191,9 +186,7 @@ const ProviderModal = () => { const handleStartFlow = () => { const providerMetadata = CONNECTORS_METADATA[selectedProvider.category][selectedProvider.provider]; - if (providerMetadata.authStrategy.strategy === AuthStrategy.api_key) { - setOpenApiKeyDialog(true); - } else if(providerMetadata.authStrategy.strategy === AuthStrategy.basic) { + if (providerMetadata.authStrategy.strategy === AuthStrategy.api_key || providerMetadata.authStrategy.strategy === AuthStrategy.basic) { setOpenBasicAuthDialog(true); } else if (providerMetadata?.options?.end_user_domain) { setOpenDomainDialog(true); @@ -237,60 +230,11 @@ const ProviderModal = () => { }); } - const onCloseApiKeyDialog = (dialogState : boolean) => { - setOpenApiKeyDialog(dialogState); - reset(); - } - const onCloseBasicAuthDialog = (dialogState : boolean) => { setOpenBasicAuthDialog(dialogState); reset2(); } - - const onApiKeySubmit = (values: IApiKeyFormData) => { - // const extraFields = getValues() - onCloseApiKeyDialog(false); - setLoading({status: true, provider: selectedProvider?.provider!}); - setPreStartFlow(false); - - // Creating API Key Connection - createApiKeyConnection({ - query : { - linkedUserId: magicLink?.id_linked_user as string, - projectId: projectId, - providerName: selectedProvider?.provider!, - vertical: selectedProvider?.category! - }, - data: values - }, - { - onSuccess: () => { - setSelectedProvider({ - provider: '', - category: '' - }); - - setLoading({ - status: false, - provider: '' - }); - setOpenSuccessDialog(true); - }, - onError: (error) => { - setErrorResponse({errorPresent:true,errorMessage: error.message}); - setLoading({ - status: false, - provider: '' - }); - setSelectedProvider({ - provider: '', - category: '' - }); - } - }); - } - const onBasicAuthSubmit = (values: IBasicAuthFormData) => { onCloseBasicAuthDialog(false); setLoading({status: true, provider: selectedProvider?.provider!}); @@ -402,63 +346,6 @@ const ProviderModal = () => { - {/* Dialog for apikey input */} - - - - Enter a API key - - {/*
*/} - -
-
- - -
{errors.apikey && (

{errors.apikey.message}

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

{errors[fieldName]?.message}

)} - - ))} -
- - - - - -
- {/* */} -
-
- {/* Dialog for basic auth input */} @@ -472,7 +359,7 @@ const ProviderModal = () => { {selectedProvider.provider!=='' && selectedProvider.category!=='' && CONNECTORS_METADATA[selectedProvider.category][selectedProvider.provider].authStrategy.properties?.map((fieldName : string) => ( <> - + {
{ e.preventDefault(); onDomainSubmit(); }}>
-
diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index b104affb9..37071eb07 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -189,7 +189,7 @@ model crm_contacts { model crm_deals { id_crm_deal String @id(map: "pk_crm_deal") @db.Uuid name String - description String + description String? amount BigInt created_at DateTime @db.Timestamp(6) modified_at DateTime @db.Timestamp(6) @@ -503,6 +503,7 @@ model tcg_collections { 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 @@ -517,14 +518,14 @@ model tcg_comments { is_private Boolean? remote_id String? remote_platform String? - created_at DateTime? @db.Timestamp(6) - modified_at DateTime? @db.Timestamp(6) 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") @@ -595,14 +596,14 @@ model tcg_tickets { collections String[] completed_at DateTime? @db.Timestamp(6) priority String? - created_at DateTime @db.Timestamp(6) - modified_at DateTime @db.Timestamp(6) assigned_to String[] remote_id String? remote_platform String? creator_type String? id_tcg_user String? @db.Uuid - id_linked_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[] @@ -619,10 +620,10 @@ model tcg_users { remote_id String? remote_platform String? teams String[] - created_at DateTime? @db.Timestamp(6) - modified_at DateTime? @db.Timestamp(6) 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[] } @@ -677,7 +678,6 @@ model connector_sets { crm_zendesk Boolean? crm_close Boolean? fs_box Boolean? - tcg_github Boolean? projects projects[] } @@ -1640,7 +1640,6 @@ model acc_vendor_credits { accounting_period String? @db.Uuid } - model ecom_customers { id_ecom_customer String @id(map: "pk_ecom_customers") @db.Uuid remote_id String? diff --git a/packages/api/src/@core/connections/@utils/types.ts b/packages/api/src/@core/connections/@utils/types.ts index d267c9bcb..45803b43b 100644 --- a/packages/api/src/@core/connections/@utils/types.ts +++ b/packages/api/src/@core/connections/@utils/types.ts @@ -17,11 +17,10 @@ export type OAuthCallbackParams = { export type APIKeyCallbackParams = { projectId: string; linkedUserId: string; - apikey: string; - body_data?: { [key: string]: any }; + body?: { [key: string]: any }; }; -export type BasicAuthCallbackParams = Omit; +export type BasicAuthCallbackParams = APIKeyCallbackParams; // Define the discriminated union type for callback parameters export type CallbackParams = diff --git a/packages/api/src/@core/connections/ats/services/ashby/ashby.service.ts b/packages/api/src/@core/connections/ats/services/ashby/ashby.service.ts index 9ff75d4e6..77ec8a559 100644 --- a/packages/api/src/@core/connections/ats/services/ashby/ashby.service.ts +++ b/packages/api/src/@core/connections/ats/services/ashby/ashby.service.ts @@ -75,7 +75,7 @@ export class AshbyConnectionService extends AbstractBaseConnectionService { async handleCallback(opts: BasicAuthCallbackParams) { try { - const { linkedUserId, projectId, body_data } = opts; + const { linkedUserId, projectId, body } = opts; const isNotUnique = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, @@ -93,7 +93,7 @@ export class AshbyConnectionService extends AbstractBaseConnectionService { id_connection: isNotUnique.id_connection, }, data: { - access_token: this.cryptoService.encrypt(body_data.username), + access_token: this.cryptoService.encrypt(body.username), account_url: CONNECTORS_METADATA['ats']['ashby'].urls .apiUrl as string, status: 'valid', @@ -110,7 +110,7 @@ export class AshbyConnectionService extends AbstractBaseConnectionService { token_type: 'basic', account_url: CONNECTORS_METADATA['ats']['ashby'].urls .apiUrl as string, - access_token: this.cryptoService.encrypt(body_data.username), + access_token: this.cryptoService.encrypt(body.username), status: 'valid', created_at: new Date(), projects: { diff --git a/packages/api/src/@core/connections/ats/services/bamboohr/bamboohr.service.ts b/packages/api/src/@core/connections/ats/services/bamboohr/bamboohr.service.ts index 4d4627ee5..92c56609f 100644 --- a/packages/api/src/@core/connections/ats/services/bamboohr/bamboohr.service.ts +++ b/packages/api/src/@core/connections/ats/services/bamboohr/bamboohr.service.ts @@ -14,7 +14,7 @@ import { Injectable } from '@nestjs/common'; import { AuthStrategy, CONNECTORS_METADATA, - OAuth2AuthData, + DynamicApiUrl, providerToType, } from '@panora/shared'; import axios from 'axios'; @@ -91,7 +91,8 @@ export class BamboohrConnectionService extends AbstractBaseConnectionService { async handleCallback(opts: OAuthCallbackParams) { try { - const { linkedUserId, projectId, code } = opts; + const { linkedUserId, projectId, body } = opts; + const { username, company_subdomain } = body; const isNotUnique = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, @@ -100,71 +101,20 @@ export class BamboohrConnectionService extends AbstractBaseConnectionService { }, }); - //reconstruct the redirect URI that was passed in the githubend it must be the same - const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; - - const CREDENTIALS = (await this.cService.getCredentials( - projectId, - this.type, - )) as OAuth2AuthData; - - const formData = new URLSearchParams({ - redirect_uri: REDIRECT_URI, - code: code, - client_id: CREDENTIALS.CLIENT_ID, - client_secret: CREDENTIALS.CLIENT_SECRET, - grant_type: 'authorization_code', - scope: CONNECTORS_METADATA['ats']['bamboohr'].scopes, - }); - const res = await axios.post( - `https://.bamboohr.com/token.php?request=token`, - formData.toString(), - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', - }, - }, - ); - const data: BamboohrOAuthResponse = res.data; - this.logger.log( - 'OAuth credentials : bamboohr ats ' + JSON.stringify(data), - ); - - const formData_ = new URLSearchParams({ - id_token: data.id_token, - applicationKey: '', // TODO - }); - //fetch the api key of the user - const res_ = await axios.post( - `https://api.bamboohr.com/api/gateway.php/{company}/v1/oidcLogin`, - formData_.toString(), - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', - }, - }, - ); - const data_: { - success: boolean; - userId: number; - employeeId: number; - key: string; - } = res_.data; - this.logger.log( - 'OAuth credentials : bamboohr ats apikey res ' + JSON.stringify(data_), - ); - let db_res; const connection_token = uuidv4(); - + const BASE_API_URL = ( + CONNECTORS_METADATA['ats']['bamboohr'].urls + .apiUrl as DynamicApiUrl + )(company_subdomain); if (isNotUnique) { db_res = await this.prisma.connections.update({ where: { id_connection: isNotUnique.id_connection, }, data: { - access_token: this.cryptoService.encrypt(data_.key), - account_url: `https://api.bamboohr.com/api/gateway.php/{company}`, + access_token: this.cryptoService.encrypt(username), + account_url: BASE_API_URL, status: 'valid', created_at: new Date(), }, @@ -176,9 +126,9 @@ export class BamboohrConnectionService extends AbstractBaseConnectionService { connection_token: connection_token, provider_slug: 'bamboohr', vertical: 'ats', - token_type: 'oauth2', - account_url: `https://api.bamboohr.com/api/gateway.php/{company}`, - access_token: this.cryptoService.encrypt(data_.key), + token_type: 'basic', + account_url: BASE_API_URL, + access_token: this.cryptoService.encrypt(username), status: 'valid', created_at: new Date(), projects: { diff --git a/packages/api/src/@core/connections/ats/services/workday/workday.service.ts b/packages/api/src/@core/connections/ats/services/workday/workday.service.ts index 6c5206b79..31ead4988 100644 --- a/packages/api/src/@core/connections/ats/services/workday/workday.service.ts +++ b/packages/api/src/@core/connections/ats/services/workday/workday.service.ts @@ -88,9 +88,9 @@ export class WorkdayConnectionService extends AbstractBaseConnectionService { // construct a custom key as workday asks for 3 params (X-Api-Key, X-User-Token, X-User-Email) // we simply use the string panoradelimiter to separate and encode easily const data_to_encode = this.connectionUtils.applyPanoraDelimiter([ - opts.apikey, - opts.body_data.userToken, - opts.body_data.userEmail, + opts.body.api_key, + opts.body.userToken, + opts.body.userEmail, ]); if (isNotUnique) { diff --git a/packages/api/src/@core/connections/connections.controller.ts b/packages/api/src/@core/connections/connections.controller.ts index 6e15879ad..6beee5fcd 100644 --- a/packages/api/src/@core/connections/connections.controller.ts +++ b/packages/api/src/@core/connections/connections.controller.ts @@ -168,31 +168,21 @@ export class ConnectionsController { const stateData: StateDataType = JSON.parse(decodeURIComponent(state)); const { projectId, vertical, linkedUserId, providerName } = stateData; - const { apikey, ...body_data } = body; const strategy = CONNECTORS_METADATA[vertical.toLowerCase()][providerName.toLowerCase()] .authStrategy.strategy; - const body_ = - strategy == AuthStrategy.api_key - ? { - projectId, - linkedUserId, - apikey, - body_data, - } - : { - projectId, - linkedUserId, - body_data, - }; const strategy_type = strategy == AuthStrategy.api_key ? 'apikey' : 'basic'; const service = this.categoryConnectionRegistry.getService( vertical.toLowerCase(), ); - await service.handleCallBack(providerName, body_, strategy_type); + await service.handleCallBack(providerName, { + projectId, + linkedUserId, + body, + }, strategy_type); /*if ( CONNECTORS_METADATA[vertical.toLowerCase()][providerName.toLowerCase()] .active !== false diff --git a/packages/api/src/@core/connections/crm/services/affinity/affinity.service.ts b/packages/api/src/@core/connections/crm/services/affinity/affinity.service.ts index 6215df6cd..ad5f0294f 100644 --- a/packages/api/src/@core/connections/crm/services/affinity/affinity.service.ts +++ b/packages/api/src/@core/connections/crm/services/affinity/affinity.service.ts @@ -74,7 +74,7 @@ export class AffinityConnectionService extends AbstractBaseConnectionService { async handleCallback(opts: BasicAuthCallbackParams) { try { - const { linkedUserId, projectId, body_data } = opts; + const { linkedUserId, projectId, body } = opts; const isNotUnique = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, @@ -92,7 +92,7 @@ export class AffinityConnectionService extends AbstractBaseConnectionService { id_connection: isNotUnique.id_connection, }, data: { - access_token: this.cryptoService.encrypt(body_data.password), + access_token: this.cryptoService.encrypt(body.password), account_url: CONNECTORS_METADATA['crm']['affinity'].urls .apiUrl as string, status: 'valid', @@ -109,7 +109,7 @@ export class AffinityConnectionService extends AbstractBaseConnectionService { token_type: 'basic', account_url: CONNECTORS_METADATA['crm']['affinity'].urls .apiUrl as string, - access_token: this.cryptoService.encrypt(body_data.password), + access_token: this.cryptoService.encrypt(body.password), status: 'valid', created_at: new Date(), projects: { diff --git a/packages/api/src/@core/connections/ecommerce/ecommerce.connection.module.ts b/packages/api/src/@core/connections/ecommerce/ecommerce.connection.module.ts index 668914d7a..a8bfd2543 100644 --- a/packages/api/src/@core/connections/ecommerce/ecommerce.connection.module.ts +++ b/packages/api/src/@core/connections/ecommerce/ecommerce.connection.module.ts @@ -7,6 +7,7 @@ import { Module } from '@nestjs/common'; import { EcommerceConnectionsService } from './services/ecommerce.connection.service'; import { ServiceRegistry } from './services/registry.service'; import { ShopifyConnectionService } from './services/shopify/shopify.service'; +import { WoocommerceConnectionService } from './services/woocommerce/woocommerce.service'; @Module({ imports: [WebhookModule, BullQueueModule], @@ -18,6 +19,7 @@ import { ShopifyConnectionService } from './services/shopify/shopify.service'; ConnectionsStrategiesService, //PROVIDERS SERVICES, ShopifyConnectionService, + WoocommerceConnectionService, ], exports: [EcommerceConnectionsService], }) diff --git a/packages/api/src/@core/connections/ecommerce/services/shopify/shopify.service.ts b/packages/api/src/@core/connections/ecommerce/services/shopify/shopify.service.ts index 66fa07f0a..484fb5952 100644 --- a/packages/api/src/@core/connections/ecommerce/services/shopify/shopify.service.ts +++ b/packages/api/src/@core/connections/ecommerce/services/shopify/shopify.service.ts @@ -77,7 +77,7 @@ export class ShopifyConnectionService extends AbstractBaseConnectionService { data: config.data, headers: config.headers, }, - 'ecommerce.ashby.passthrough', + 'ecommerce.shopify.passthrough', config.linkedUserId, ); } catch (error) { @@ -91,54 +91,29 @@ export class ShopifyConnectionService extends AbstractBaseConnectionService { async handleCallback(opts: OAuthCallbackParams) { try { - const { linkedUserId, projectId, code, hmac, shop } = opts; + const { linkedUserId, projectId, body } = opts; + const { api_key, store_url } = body; const isNotUnique = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, - provider_slug: 'shopify', + provider_slug: 'woocommerce', vertical: 'ecommerce', }, }); - const shopRegex = /^[a-zA-Z0-9][a-zA-Z0-9\-]*\.myshopify\.com/; - - const CREDENTIALS = (await this.cService.getCredentials( - projectId, - this.type, - )) as OAuth2AuthData; - - if (!shopRegex.test(shop)) { - throw new Error('Invalid shop received through shopify request'); - } - //todo: check hmac - const formData = new URLSearchParams({ - code: code, - client_id: CREDENTIALS.CLIENT_ID, - client_secret: CREDENTIALS.CLIENT_SECRET, - }); - const res = await axios.post( - `https://${shop}.myshopify.com/admin/oauth/access_token`, - formData.toString(), - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', - }, - }, - ); - const data: ShopifyOAuthResponse = res.data; - let db_res; const connection_token = uuidv4(); const BASE_API_URL = ( - CONNECTORS_METADATA['ecommerce']['shopify'].urls.apiUrl as DynamicApiUrl - )(shop); + CONNECTORS_METADATA['ecommerce']['shopify'].urls + .apiUrl as DynamicApiUrl + )(store_url); if (isNotUnique) { db_res = await this.prisma.connections.update({ where: { id_connection: isNotUnique.id_connection, }, data: { - access_token: this.cryptoService.encrypt(data.access_token), + access_token: this.cryptoService.encrypt(api_key), account_url: BASE_API_URL, status: 'valid', created_at: new Date(), @@ -151,9 +126,9 @@ export class ShopifyConnectionService extends AbstractBaseConnectionService { connection_token: connection_token, provider_slug: 'shopify', vertical: 'ecommerce', - token_type: 'oauth2', + token_type: 'basic', account_url: BASE_API_URL, - access_token: this.cryptoService.encrypt(data.access_token), + access_token: this.cryptoService.encrypt(api_key), status: 'valid', created_at: new Date(), projects: { diff --git a/packages/api/src/@core/connections/ecommerce/services/woocommerce/woocommerce.service.ts b/packages/api/src/@core/connections/ecommerce/services/woocommerce/woocommerce.service.ts new file mode 100644 index 000000000..e53263ef4 --- /dev/null +++ b/packages/api/src/@core/connections/ecommerce/services/woocommerce/woocommerce.service.ts @@ -0,0 +1,155 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { RetryHandler } from '@@core/@core-services/request-retry/retry.handler'; +import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { + AbstractBaseConnectionService, + OAuthCallbackParams, + PassthroughInput, + RefreshParams, +} from '@@core/connections/@utils/types'; +import { PassthroughResponse } from '@@core/passthrough/types'; +import { Injectable } from '@nestjs/common'; +import { + AuthStrategy, + CONNECTORS_METADATA, + DynamicApiUrl, + providerToType, +} from '@panora/shared'; +import { v4 as uuidv4 } from 'uuid'; +import { ServiceRegistry } from '../registry.service'; + +@Injectable() +export class WoocommerceConnectionService extends AbstractBaseConnectionService { + private readonly type: string; + + constructor( + protected prisma: PrismaService, + private logger: LoggerService, + protected cryptoService: EncryptionService, + private registry: ServiceRegistry, + private connectionUtils: ConnectionUtils, + private cService: ConnectionsStrategiesService, + private retryService: RetryHandler, + ) { + super(prisma, cryptoService); + this.logger.setContext(WoocommerceConnectionService.name); + this.registry.registerService('woocommerce', this); + this.type = providerToType('woocommerce', 'ecommerce', AuthStrategy.oauth2); + } + + async passthrough( + input: PassthroughInput, + connectionId: string, + ): Promise { + try { + const { headers } = input; + const config = await this.constructPassthrough(input, connectionId); + + const connection = await this.prisma.connections.findUnique({ + where: { + id_connection: connectionId, + }, + }); + + const decryptedData = JSON.parse( + this.cryptoService.decrypt(connection.access_token), + ); + + const { username, password } = decryptedData; + + config.headers = { + ...config.headers, + ...headers, + Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString( + 'base64', + )}`, + }; + + return await this.retryService.makeRequest( + { + method: config.method, + url: config.url, + data: config.data, + headers: config.headers, + }, + 'ecommerce.woocommerce.passthrough', + config.linkedUserId, + ); + } catch (error) { + throw error; + } + } + + async handleCallback(opts: OAuthCallbackParams) { + try { + const { linkedUserId, projectId, body } = opts; + const { username, password, store_url } = body; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'woocommerce', + vertical: 'ecommerce', + }, + }); + + let db_res; + const connection_token = uuidv4(); + const BASE_API_URL = ( + CONNECTORS_METADATA['ecommerce']['woocommerce'].urls + .apiUrl as DynamicApiUrl + )(store_url); + if (isNotUnique) { + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt( + JSON.stringify({ username, password }), + ), + account_url: BASE_API_URL, + status: 'valid', + created_at: new Date(), + }, + }); + } else { + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + connection_token: connection_token, + provider_slug: 'woocommerce', + vertical: 'ecommerce', + token_type: 'basic', + account_url: BASE_API_URL, + access_token: this.cryptoService.encrypt( + JSON.stringify({ username, password }), + ), + status: 'valid', + created_at: new Date(), + projects: { + connect: { id_project: projectId }, + }, + linked_users: { + connect: { + id_linked_user: await this.connectionUtils.getLinkedUserId( + projectId, + linkedUserId, + ), + }, + }, + }, + }); + } + return db_res; + } catch (error) { + throw error; + } + } + + handleTokenRefresh?(opts: RefreshParams): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/api/src/@core/connections/marketingautomation/services/brevo/brevo.service.ts b/packages/api/src/@core/connections/marketingautomation/services/brevo/brevo.service.ts index c9172b3fd..c8a92e2ea 100644 --- a/packages/api/src/@core/connections/marketingautomation/services/brevo/brevo.service.ts +++ b/packages/api/src/@core/connections/marketingautomation/services/brevo/brevo.service.ts @@ -94,7 +94,7 @@ export class BrevoConnectionService extends AbstractBaseConnectionService { id_connection: isNotUnique.id_connection, }, data: { - access_token: this.cryptoService.encrypt(opts.apikey), + access_token: this.cryptoService.encrypt(opts.body.api_key), account_url: CONNECTORS_METADATA['marketingautomation']['brevo'] .urls.apiUrl as string, status: 'valid', @@ -111,7 +111,7 @@ export class BrevoConnectionService extends AbstractBaseConnectionService { token_type: 'api_key', account_url: CONNECTORS_METADATA['marketingautomation']['brevo'] .urls.apiUrl as string, - access_token: this.cryptoService.encrypt(opts.apikey), + access_token: this.cryptoService.encrypt(opts.body.api_key), status: 'valid', created_at: new Date(), projects: { diff --git a/packages/api/src/@core/connections/ticketing/services/dixa/dixa.service.ts b/packages/api/src/@core/connections/ticketing/services/dixa/dixa.service.ts index 818e02b76..1698fc730 100644 --- a/packages/api/src/@core/connections/ticketing/services/dixa/dixa.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/dixa/dixa.service.ts @@ -76,7 +76,7 @@ export class DixaConnectionService extends AbstractBaseConnectionService { async handleCallback(opts: APIKeyCallbackParams) { try { - const { linkedUserId, projectId } = opts; + const { linkedUserId, projectId, body } = opts; const isNotUnique = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, @@ -94,7 +94,7 @@ export class DixaConnectionService extends AbstractBaseConnectionService { id_connection: isNotUnique.id_connection, }, data: { - access_token: this.cryptoService.encrypt(opts.apikey), + access_token: this.cryptoService.encrypt(body.api_key), account_url: CONNECTORS_METADATA['ticketing']['dixa'].urls .apiUrl as string, status: 'valid', @@ -111,7 +111,7 @@ export class DixaConnectionService extends AbstractBaseConnectionService { token_type: 'api_key', account_url: CONNECTORS_METADATA['ticketing']['dixa'].urls .apiUrl as string, - access_token: this.cryptoService.encrypt(opts.apikey), + access_token: this.cryptoService.encrypt(body.api_key), status: 'valid', created_at: new Date(), projects: { diff --git a/packages/api/src/@core/utils/dtos/query.dto.ts b/packages/api/src/@core/utils/dtos/query.dto.ts index 1f03c8de9..6a0385e97 100644 --- a/packages/api/src/@core/utils/dtos/query.dto.ts +++ b/packages/api/src/@core/utils/dtos/query.dto.ts @@ -1,9 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; +import { Transform, Type } from 'class-transformer'; import { IsBoolean, IsNumber, IsOptional, IsUUID } from 'class-validator'; // To provide a default limit -const DEFAULT_PAGE_SIZE = 30; +export const DEFAULT_PAGE_SIZE = 50; export class QueryDto { @ApiProperty({ @@ -27,9 +27,10 @@ export class QueryDto { required: false, description: 'Set to get the number of records.', }) + @Type(() => Number) @IsOptional() @IsNumber() - @Transform((p) => Number(p.value)) + @Transform((value) => Number(value)) limit: number = DEFAULT_PAGE_SIZE; @ApiProperty({ diff --git a/packages/api/src/@core/utils/types/original/original.ecommerce.ts b/packages/api/src/@core/utils/types/original/original.ecommerce.ts index 7e59de0fa..ee02ae1ae 100644 --- a/packages/api/src/@core/utils/types/original/original.ecommerce.ts +++ b/packages/api/src/@core/utils/types/original/original.ecommerce.ts @@ -4,6 +4,10 @@ import { ShopifyCustomerInput, ShopifyCustomerOutput, } from '@ecommerce/customer/services/shopify/types'; +import { + WoocommerceCustomerInput, + WoocommerceCustomerOutput, +} from '@ecommerce/customer/services/woocommerce/types'; import { ShopifyFulfillmentInput, ShopifyFulfillmentOutput, @@ -16,22 +20,31 @@ import { ShopifyOrderInput, ShopifyOrderOutput, } from '@ecommerce/order/services/shopify/types'; +import { + WoocommerceOrderInput, + WoocommerceOrderOutput, +} from '@ecommerce/order/services/woocommerce/types'; import { ShopifyProductInput, ShopifyProductOutput, } from '@ecommerce/product/services/shopify/types'; +import { WoocommerceProductOutput } from '@ecommerce/product/services/woocommerce/types'; /* product */ -export type OriginalProductInput = ShopifyProductInput; +export type OriginalProductInput = + | ShopifyProductInput + | WoocommerceProductOutput; /* order */ -export type OriginalOrderInput = ShopifyOrderInput; +export type OriginalOrderInput = ShopifyOrderInput | WoocommerceOrderInput; /* fulfillmentorders */ export type OriginalFulfillmentOrdersInput = ShopifyFulfillmentOrdersInput; /* customer */ -export type OriginalCustomerInput = ShopifyCustomerInput; +export type OriginalCustomerInput = + | ShopifyCustomerInput + | WoocommerceCustomerInput; /* fulfillment */ export type OriginalFulfillmentInput = ShopifyFulfillmentInput; @@ -46,16 +59,20 @@ export type EcommerceObjectInput = /* OUTPUT */ /* product */ -export type OriginalProductOutput = ShopifyProductOutput; +export type OriginalProductOutput = + | ShopifyProductOutput + | WoocommerceProductOutput; /* order */ -export type OriginalOrderOutput = ShopifyOrderOutput; +export type OriginalOrderOutput = ShopifyOrderOutput | WoocommerceOrderOutput; /* fulfillmentorders */ export type OriginalFulfillmentOrdersOutput = ShopifyFulfillmentOrdersOutput; /* customer */ -export type OriginalCustomerOutput = ShopifyCustomerOutput; +export type OriginalCustomerOutput = + | ShopifyCustomerOutput + | WoocommerceCustomerOutput; /* fulfillment */ export type OriginalFulfillmentOutput = ShopifyFulfillmentOutput; diff --git a/packages/api/src/ecommerce/customer/customer.controller.ts b/packages/api/src/ecommerce/customer/customer.controller.ts index 5438e6fe4..cd2f33b5d 100644 --- a/packages/api/src/ecommerce/customer/customer.controller.ts +++ b/packages/api/src/ecommerce/customer/customer.controller.ts @@ -9,6 +9,8 @@ import { Param, Query, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { ApiHeader, @@ -46,6 +48,7 @@ export class CustomerController { example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) @ApiPaginatedResponse(UnifiedEcommerceCustomerOutput) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @UseGuards(ApiKeyAuthGuard) @Get() async getCustomers( diff --git a/packages/api/src/ecommerce/customer/customer.module.ts b/packages/api/src/ecommerce/customer/customer.module.ts index 2e9f0a36e..448deb41e 100644 --- a/packages/api/src/ecommerce/customer/customer.module.ts +++ b/packages/api/src/ecommerce/customer/customer.module.ts @@ -1,15 +1,16 @@ +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 { Utils } from '@ecommerce/@lib/@utils'; import { Module } from '@nestjs/common'; import { CustomerController } from './customer.controller'; import { CustomerService } from './services/customer.service'; import { ServiceRegistry } from './services/registry.service'; import { ShopifyService } from './services/shopify'; -import { SyncService } from './sync/sync.service'; -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; -import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; -import { Utils } from '@ecommerce/@lib/@utils'; import { ShopifyCustomerMapper } from './services/shopify/mappers'; +import { WoocommerceService } from './services/woocommerce'; +import { WoocommerceCustomerMapper } from './services/woocommerce/mappers'; +import { SyncService } from './sync/sync.service'; @Module({ controllers: [CustomerController], @@ -22,8 +23,10 @@ import { ShopifyCustomerMapper } from './services/shopify/mappers'; IngestDataService, Utils, ShopifyCustomerMapper, + WoocommerceCustomerMapper, /* PROVIDERS SERVICES */ ShopifyService, + WoocommerceService, ], exports: [SyncService], }) diff --git a/packages/api/src/ecommerce/customer/services/woocommerce/index.ts b/packages/api/src/ecommerce/customer/services/woocommerce/index.ts new file mode 100644 index 000000000..2ccd22eac --- /dev/null +++ b/packages/api/src/ecommerce/customer/services/woocommerce/index.ts @@ -0,0 +1,66 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.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 { ICustomerService } from '@ecommerce/customer/types'; +import { Injectable } from '@nestjs/common'; +import { EcommerceObject } from '@panora/shared'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { WoocommerceCustomerOutput } from './types'; + +@Injectable() +export class WoocommerceService implements ICustomerService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + EcommerceObject.customer.toUpperCase() + ':' + WoocommerceService.name, + ); + this.registry.registerService('woocommerce', this); + } + + async sync( + data: SyncParam, + ): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'woocommerce', + vertical: 'ecommerce', + }, + }); + const decryptedData = JSON.parse( + this.cryptoService.decrypt(connection.access_token), + ); + + const { username, password } = decryptedData; + + const resp = await axios.get(`${connection.account_url}/v3/customers`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${Buffer.from( + `${username}:${password}`, + ).toString('base64')}`, + }, + }); + const customers: WoocommerceCustomerOutput[] = resp.data; + this.logger.log(`Synced woocommerce customers !`); + + return { + data: customers, + message: 'Woocommerce customers retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/ecommerce/customer/services/woocommerce/mappers.ts b/packages/api/src/ecommerce/customer/services/woocommerce/mappers.ts new file mode 100644 index 000000000..22c6d715d --- /dev/null +++ b/packages/api/src/ecommerce/customer/services/woocommerce/mappers.ts @@ -0,0 +1,184 @@ +import { WoocommerceCustomerInput, WoocommerceCustomerOutput } from './types'; +import { + UnifiedEcommerceCustomerInput, + UnifiedEcommerceCustomerOutput, +} from '@ecommerce/customer/types/model.unified'; +import { ICustomerMapper } from '@ecommerce/customer/types'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { Injectable } from '@nestjs/common'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { Utils } from '@ecommerce/@lib/@utils'; + +@Injectable() +export class WoocommerceCustomerMapper implements ICustomerMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService( + 'ecommerce', + 'customer', + 'woocommerce', + this, + ); + } + + async desunify( + source: UnifiedEcommerceCustomerInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: Partial = { + email: source.email, + first_name: source.first_name, + last_name: source.last_name, + username: source.email, // Assuming email is used as username + billing: { + first_name: source.first_name, + last_name: source.last_name, + email: source.email, + phone: source.phone_number, + }, + shipping: { + first_name: source.first_name, + last_name: source.last_name, + }, + meta_data: [], + }; + + if (source.addresses && source.addresses.length > 0) { + const primaryAddress = source.addresses[0]; + result.billing = { + ...result.billing, + address_1: primaryAddress.street_1, + address_2: primaryAddress.street_2, + city: primaryAddress.city, + state: primaryAddress.state, + postcode: primaryAddress.postal_code, + country: primaryAddress.country, + }; + result.shipping = { + ...result.shipping, + address_1: primaryAddress.street_1, + address_2: primaryAddress.street_2, + city: primaryAddress.city, + state: primaryAddress.state, + postcode: primaryAddress.postal_code, + country: primaryAddress.country, + }; + } + + if (customFieldMappings && source.field_mappings) { + result.meta_data = customFieldMappings.map((mapping) => ({ + id: 0, // WooCommerce will assign the actual ID + key: mapping.slug, + value: source.field_mappings[mapping.remote_id] || '', + })); + } + + return result as WoocommerceCustomerInput; + } + + async unify( + source: WoocommerceCustomerOutput | WoocommerceCustomerOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise< + UnifiedEcommerceCustomerOutput | UnifiedEcommerceCustomerOutput[] + > { + if (!Array.isArray(source)) { + return this.mapSingleCustomerToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((customer) => + this.mapSingleCustomerToUnified( + customer, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private mapSingleCustomerToUnified( + customer: WoocommerceCustomerOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedEcommerceCustomerOutput { + const result: UnifiedEcommerceCustomerOutput = { + remote_id: customer.id?.toString(), + remote_data: customer, + email: customer.email, + first_name: customer.first_name || null, + last_name: customer.last_name || null, + phone_number: customer.billing?.phone || null, + addresses: [], + field_mappings: {}, + }; + + if (customer.billing) { + if ( + customer.billing.address_1 && + customer.billing.city && + customer.billing.state && + customer.billing.postcode && + customer.billing.country + ) { + result.addresses.push({ + address_type: 'BILLING', + street_1: customer.billing.address_1, + street_2: customer.billing.address_2, + city: customer.billing.city, + state: customer.billing.state, + postal_code: customer.billing.postcode, + country: customer.billing.country, + }); + } + } + + if (customer.shipping) { + if ( + customer.billing.address_1 && + customer.billing.city && + customer.billing.state && + customer.billing.postcode && + customer.billing.country + ) { + result.addresses.push({ + address_type: 'SHIPPING', + street_1: customer.billing.address_1, + street_2: customer.billing.address_2, + city: customer.billing.city, + state: customer.billing.state, + postal_code: customer.billing.postcode, + country: customer.billing.country, + }); + } + } + + if (customFieldMappings && customer.meta_data) { + result.field_mappings = customer.meta_data.reduce((acc, meta) => { + const mapping = customFieldMappings.find((m) => m.slug === meta.key); + if (mapping) { + acc[mapping.remote_id] = meta.value; + } + return acc; + }, {} as Record); + } + + return result; + } +} diff --git a/packages/api/src/ecommerce/customer/services/woocommerce/types.ts b/packages/api/src/ecommerce/customer/services/woocommerce/types.ts new file mode 100644 index 000000000..da853a6d0 --- /dev/null +++ b/packages/api/src/ecommerce/customer/services/woocommerce/types.ts @@ -0,0 +1,53 @@ +export interface WoocommerceCustomerInput { + // Customer properties + readonly id: number; + readonly date_created: string; // date-time + readonly date_created_gmt: string; // date-time + readonly date_modified: string; // date-time + readonly date_modified_gmt: string; // date-time + email: string; + first_name?: string; + last_name?: string; + readonly role: string; + username?: string; + password?: string; // write-only + billing: BillingAddress; + shipping: ShippingAddress; + readonly is_paying_customer: boolean; + readonly avatar_url: string; + meta_data: MetaData[]; +} + +interface BillingAddress { + first_name?: string; + last_name?: string; + company?: string; + address_1?: string; + address_2?: string; + city?: string; + state?: string; + postcode?: string; + country?: string; + email?: string; + phone?: string; +} + +interface ShippingAddress { + first_name?: string; + last_name?: string; + company?: string; + address_1?: string; + address_2?: string; + city?: string; + state?: string; + postcode?: string; + country?: string; +} + +interface MetaData { + readonly id: number; + key: string; + value: string; +} + +export type WoocommerceCustomerOutput = Partial; diff --git a/packages/api/src/ecommerce/customer/sync/sync.service.ts b/packages/api/src/ecommerce/customer/sync/sync.service.ts index 932f6967a..a8cb3408f 100644 --- a/packages/api/src/ecommerce/customer/sync/sync.service.ts +++ b/packages/api/src/ecommerce/customer/sync/sync.service.ts @@ -189,6 +189,12 @@ export class SyncService implements OnModuleInit, IBaseSync { data: data, }); } else { + console.log( + 'addy is ' + + JSON.stringify(data) + + ' id is ' + + existingCustomer.id_ecom_customer, + ); return this.prisma.ecom_addresses.create({ data: { ...data, @@ -210,13 +216,20 @@ export class SyncService implements OnModuleInit, IBaseSync { id_ecom_customer: uuidv4(), created_at: new Date(), remote_id: originId, + remote_deleted: false, id_connection: connection_id, }, }); if (normalizedAddresses && normalizedAddresses.length > 0) { await Promise.all( - normalizedAddresses.map((data) => + normalizedAddresses.map((data) => { + console.log( + 'addy is ' + + JSON.stringify(data) + + ' id is ' + + newCus.id_ecom_customer, + ); this.prisma.ecom_addresses.create({ data: { ...data, @@ -225,8 +238,8 @@ export class SyncService implements OnModuleInit, IBaseSync { remote_deleted: false, //todo: id_connection: connection_id, }, - }), - ), + }); + }), ); } diff --git a/packages/api/src/ecommerce/fulfillment/fulfillment.controller.ts b/packages/api/src/ecommerce/fulfillment/fulfillment.controller.ts index 02a784821..28786a63d 100644 --- a/packages/api/src/ecommerce/fulfillment/fulfillment.controller.ts +++ b/packages/api/src/ecommerce/fulfillment/fulfillment.controller.ts @@ -9,6 +9,8 @@ import { Param, Query, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { ApiHeader, @@ -46,6 +48,7 @@ export class FulfillmentController { example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) @ApiPaginatedResponse(UnifiedEcommerceFulfillmentOutput) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @UseGuards(ApiKeyAuthGuard) @Get() async getFulfillments( diff --git a/packages/api/src/ecommerce/fulfillmentorders/fulfillmentorders.controller.ts b/packages/api/src/ecommerce/fulfillmentorders/fulfillmentorders.controller.ts index 8c7b0c92b..6f9d219fb 100644 --- a/packages/api/src/ecommerce/fulfillmentorders/fulfillmentorders.controller.ts +++ b/packages/api/src/ecommerce/fulfillmentorders/fulfillmentorders.controller.ts @@ -42,6 +42,7 @@ export class FulfillmentOrdersController { example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) @ApiCustomResponse(UnifiedEcommerceFulfillmentOrdersOutput) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @UseGuards(ApiKeyAuthGuard) @Get() async getFulfillmentOrderss( diff --git a/packages/api/src/ecommerce/order/order.controller.ts b/packages/api/src/ecommerce/order/order.controller.ts index c6e8a48bd..f7687d742 100644 --- a/packages/api/src/ecommerce/order/order.controller.ts +++ b/packages/api/src/ecommerce/order/order.controller.ts @@ -11,6 +11,8 @@ import { Post, Query, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { ApiBody, @@ -53,6 +55,7 @@ export class OrderController { example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) @ApiPaginatedResponse(UnifiedEcommerceOrderOutput) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @UseGuards(ApiKeyAuthGuard) @Get() async getOrders( diff --git a/packages/api/src/ecommerce/order/order.module.ts b/packages/api/src/ecommerce/order/order.module.ts index fbf9c26d3..dee1439d7 100644 --- a/packages/api/src/ecommerce/order/order.module.ts +++ b/packages/api/src/ecommerce/order/order.module.ts @@ -1,14 +1,15 @@ -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; 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 { Utils } from '@ecommerce/@lib/@utils'; import { Module } from '@nestjs/common'; import { OrderController } from './order.controller'; -import { ShopifyService } from './services/shopify'; -import { ShopifyOrderMapper } from './services/shopify/mappers'; import { OrderService } from './services/order.service'; import { ServiceRegistry } from './services/registry.service'; +import { ShopifyService } from './services/shopify'; +import { ShopifyOrderMapper } from './services/shopify/mappers'; +import { WoocommerceService } from './services/woocommerce'; +import { WoocommerceOrderMapper } from './services/woocommerce/mappers'; import { SyncService } from './sync/sync.service'; @Module({ @@ -22,8 +23,10 @@ import { SyncService } from './sync/sync.service'; IngestDataService, Utils, ShopifyOrderMapper, + WoocommerceOrderMapper, /* PROVIDERS SERVICES */ ShopifyService, + WoocommerceService, ], exports: [SyncService], }) diff --git a/packages/api/src/ecommerce/order/services/woocommerce/index.ts b/packages/api/src/ecommerce/order/services/woocommerce/index.ts new file mode 100644 index 000000000..82fb6ee67 --- /dev/null +++ b/packages/api/src/ecommerce/order/services/woocommerce/index.ts @@ -0,0 +1,107 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.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 { IOrderService } from '@ecommerce/order/types'; +import { Injectable } from '@nestjs/common'; +import { EcommerceObject } from '@panora/shared'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { WoocommerceOrderInput, WoocommerceOrderOutput } from './types'; + +@Injectable() +export class WoocommerceService implements IOrderService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + EcommerceObject.order.toUpperCase() + ':' + WoocommerceService.name, + ); + this.registry.registerService('woocommerce', this); + } + + async addOrder( + orderData: WoocommerceOrderInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'woocommerce', + vertical: 'ecommerce', + }, + }); + const decryptedData = JSON.parse( + this.cryptoService.decrypt(connection.access_token), + ); + + const { username, password } = decryptedData; + + const resp = await axios.post( + `${connection.account_url}/v3/orders`, + { + order: orderData, + }, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${Buffer.from( + `${username}:${password}`, + ).toString('base64')}`, + }, + }, + ); + + return { + data: resp.data.order, + message: 'Woocommerce order created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'woocommerce', + vertical: 'ecommerce', + }, + }); + const decryptedData = JSON.parse( + this.cryptoService.decrypt(connection.access_token), + ); + + const { username, password } = decryptedData; + + const resp = await axios.get(`${connection.account_url}/v3/orders`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${Buffer.from( + `${username}:${password}`, + ).toString('base64')}`, + }, + }); + const orders: WoocommerceOrderOutput[] = resp.data; + this.logger.log(`Synced woocommerce orders !`); + + return { + data: orders, + message: 'Woocommerce orders retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/ecommerce/order/services/woocommerce/mappers.ts b/packages/api/src/ecommerce/order/services/woocommerce/mappers.ts new file mode 100644 index 000000000..4c41571aa --- /dev/null +++ b/packages/api/src/ecommerce/order/services/woocommerce/mappers.ts @@ -0,0 +1,190 @@ +import { + UnifiedEcommerceOrderInput, + UnifiedEcommerceOrderOutput, +} from '@ecommerce/order/types/model.unified'; +import { IOrderMapper } from '@ecommerce/order/types'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { Injectable } from '@nestjs/common'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { Utils } from '@ecommerce/@lib/@utils'; +import { WoocommerceOrderInput, WoocommerceOrderOutput } from './types'; +import { CurrencyCode } from '@@core/utils/types'; + +@Injectable() +export class WoocommerceOrderMapper implements IOrderMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService( + 'ecommerce', + 'order', + 'woocommerce', + this, + ); + } + + async desunify( + source: UnifiedEcommerceOrderInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: Partial = { + status: this.mapUnifiedStatusToWooCommerce(source.order_status), + currency: source.currency, + total: source.total_price?.toString(), + total_tax: source.total_tax?.toString(), + customer_id: parseInt(source.customer_id || '0'), + customer_note: '', + billing: {} as any, + shipping: {} as any, + payment_method: '', + payment_method_title: '', + transaction_id: '', + meta_data: [], + line_items: [], + shipping_lines: [], + fee_lines: [], + coupon_lines: [], + }; + + if (source.items) { + result.line_items = Object.values(source.items).map((item) => ({ + name: item.title, + product_id: 0, // You might need to map this from your unified model + variation_id: 0, // You might need to map this from your unified model + quantity: item.quantity, + tax_class: '', + subtotal: item.price.toString(), + total: (item.price * item.quantity).toString(), + sku: item.sku, + })) as any; + } + + if (customFieldMappings && source.field_mappings) { + result.meta_data = customFieldMappings.map((mapping) => ({ + id: 0, // WooCommerce will assign the actual ID + key: mapping.slug, + value: source.field_mappings[mapping.remote_id] || '', + })); + } + + return result as WoocommerceOrderInput; + } + + async unify( + source: WoocommerceOrderOutput | WoocommerceOrderOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleOrderToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((order) => + this.mapSingleOrderToUnified(order, connectionId, customFieldMappings), + ), + ); + } + + private async mapSingleOrderToUnified( + order: WoocommerceOrderOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: UnifiedEcommerceOrderOutput = { + remote_id: order.id?.toString(), + remote_data: order, + order_status: this.mapWooCommerceStatusToUnified(order.status), + order_number: order.number, + payment_status: order.status, // WooCommerce doesn't have a separate payment status + currency: order.currency as CurrencyCode, + total_price: parseFloat(order.total || '0'), + total_discount: parseFloat(order.discount_total || '0'), + total_shipping: parseFloat(order.shipping_total || '0'), + total_tax: parseFloat(order.total_tax || '0'), + fulfillment_status: order.status, // WooCommerce doesn't have a separate fulfillment status + items: {}, + field_mappings: {}, + }; + if (order.customer_id) { + result.customer_id = await this.utils.getCustomerIdFromRemote( + String(order.customer_id), + connectionId, + ); + } + + if (order.line_items) { + result.items = order.line_items.reduce((acc, item) => { + acc[item.id.toString()] = { + title: item.name, + price: parseFloat(item.price), + quantity: item.quantity, + sku: item.sku, + }; + return acc; + }, {}); + } + + if (customFieldMappings && order.meta_data) { + result.field_mappings = order.meta_data.reduce((acc, meta) => { + const mapping = customFieldMappings.find((m) => m.slug === meta.key); + if (mapping) { + acc[mapping.remote_id] = meta.value; + } + return acc; + }, {} as Record); + } + + return result; + } + + private mapUnifiedStatusToWooCommerce( + status?: string, + ): WoocommerceOrderInput['status'] { + switch (status) { + case 'PAID': + return 'processing'; + case 'UNPAID': + return 'pending'; + case 'CANCELLED': + return 'cancelled'; + case 'REFUNDED': + return 'refunded'; + default: + return 'pending'; + } + } + + private mapWooCommerceStatusToUnified( + status?: WoocommerceOrderInput['status'], + ): string { + switch (status) { + case 'processing': + case 'completed': + return 'PAID'; + case 'pending': + case 'on-hold': + return 'UNPAID'; + case 'cancelled': + return 'CANCELLED'; + case 'refunded': + return 'REFUNDED'; + default: + return 'UNPAID'; + } + } +} diff --git a/packages/api/src/ecommerce/order/services/woocommerce/types.ts b/packages/api/src/ecommerce/order/services/woocommerce/types.ts new file mode 100644 index 000000000..6e8430bf3 --- /dev/null +++ b/packages/api/src/ecommerce/order/services/woocommerce/types.ts @@ -0,0 +1,148 @@ +export interface WoocommerceOrderInput { + readonly id: number; + parent_id?: number; + readonly number: string; + readonly order_key: string; + readonly created_via: string; + readonly version: string; + status: + | 'pending' + | 'processing' + | 'on-hold' + | 'completed' + | 'cancelled' + | 'refunded' + | 'failed' + | 'trash'; + currency: string; + readonly date_created: string; + readonly date_created_gmt: string; + readonly date_modified: string; + readonly date_modified_gmt: string; + readonly discount_total: string; + readonly discount_tax: string; + readonly shipping_total: string; + readonly shipping_tax: string; + readonly cart_tax: string; + readonly total: string; + readonly total_tax: string; + readonly prices_include_tax: boolean; + customer_id: number; + readonly customer_ip_address: string; + readonly customer_user_agent: string; + customer_note: string; + billing: BillingAddress; + shipping: ShippingAddress; + payment_method: string; + payment_method_title: string; + transaction_id: string; + readonly date_paid: string; + readonly date_paid_gmt: string; + readonly date_completed: string; + readonly date_completed_gmt: string; + readonly cart_hash: string; + meta_data: MetaData[]; + line_items: LineItem[]; + readonly tax_lines: TaxLine[]; + shipping_lines: ShippingLine[]; + fee_lines: FeeLine[]; + coupon_lines: CouponLine[]; + readonly refunds: Refund[]; + set_paid?: boolean; +} + +interface BillingAddress { + first_name: string; + last_name: string; + company: string; + address_1: string; + address_2: string; + city: string; + state: string; + postcode: string; + country: string; + email: string; + phone: string; +} + +interface ShippingAddress { + first_name: string; + last_name: string; + company: string; + address_1: string; + address_2: string; + city: string; + state: string; + postcode: string; + country: string; +} + +interface MetaData { + readonly id: number; + key: string; + value: string; +} + +interface LineItem { + readonly id: number; + name: string; + product_id: number; + variation_id: number; + quantity: number; + tax_class: string; + subtotal: string; + readonly subtotal_tax: string; + total: string; + readonly total_tax: string; + readonly taxes: TaxLine[]; + meta_data: MetaData[]; + readonly sku: string; + readonly price: string; +} + +interface TaxLine { + readonly id: number; + readonly rate_code: string; + readonly rate_id: number; + readonly label: string; + readonly compound: boolean; + readonly tax_total: string; + readonly shipping_tax_total: string; + meta_data: MetaData[]; +} + +interface ShippingLine { + readonly id: number; + method_title: string; + method_id: string; + total: string; + readonly total_tax: string; + readonly taxes: TaxLine[]; + meta_data: MetaData[]; +} + +interface FeeLine { + readonly id: number; + name: string; + tax_class: string; + tax_status: 'taxable' | 'none'; + total: string; + readonly total_tax: string; + readonly taxes: TaxLine[]; + meta_data: MetaData[]; +} + +interface CouponLine { + readonly id: number; + code: string; + readonly discount: string; + readonly discount_tax: string; + meta_data: MetaData[]; +} + +interface Refund { + readonly id: number; + readonly reason: string; + readonly total: string; +} +export type WoocommerceOrderOutput = Partial; diff --git a/packages/api/src/ecommerce/order/sync/sync.service.ts b/packages/api/src/ecommerce/order/sync/sync.service.ts index 015494446..e24fdc2f1 100644 --- a/packages/api/src/ecommerce/order/sync/sync.service.ts +++ b/packages/api/src/ecommerce/order/sync/sync.service.ts @@ -172,6 +172,7 @@ export class SyncService implements OnModuleInit, IBaseSync { ...baseData, id_ecom_order: uuidv4(), created_at: new Date(), + remote_deleted: false, remote_id: originId, id_connection: connection_id, }, diff --git a/packages/api/src/ecommerce/order/types/index.ts b/packages/api/src/ecommerce/order/types/index.ts index b3bf36151..8dac7726d 100644 --- a/packages/api/src/ecommerce/order/types/index.ts +++ b/packages/api/src/ecommerce/order/types/index.ts @@ -1,5 +1,8 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { UnifiedEcommerceOrderInput, UnifiedEcommerceOrderOutput } from './model.unified'; +import { + UnifiedEcommerceOrderInput, + UnifiedEcommerceOrderOutput, +} from './model.unified'; import { OriginalOrderOutput } from '@@core/utils/types/original/original.ecommerce'; import { ApiResponse } from '@@core/utils/types'; import { IBaseObjectService, SyncParam } from '@@core/utils/types/interface'; diff --git a/packages/api/src/ecommerce/product/product.controller.ts b/packages/api/src/ecommerce/product/product.controller.ts index f64f2f0a8..58d229a17 100644 --- a/packages/api/src/ecommerce/product/product.controller.ts +++ b/packages/api/src/ecommerce/product/product.controller.ts @@ -6,7 +6,7 @@ import { ApiPaginatedResponse, ApiPostCustomResponse, } from '@@core/utils/dtos/openapi.respone.dto'; -import { QueryDto } from '@@core/utils/dtos/query.dto'; +import { DEFAULT_PAGE_SIZE, QueryDto } from '@@core/utils/dtos/query.dto'; import { Body, Controller, @@ -16,6 +16,8 @@ import { Post, Query, UseGuards, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { ApiBody, @@ -53,6 +55,7 @@ export class ProductController { example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) @ApiPaginatedResponse(UnifiedEcommerceProductOutput) + @UsePipes(new ValidationPipe({ transform: true, disableErrorMessages: true })) @UseGuards(ApiKeyAuthGuard) @Get() async getProducts( diff --git a/packages/api/src/ecommerce/product/product.module.ts b/packages/api/src/ecommerce/product/product.module.ts index 1d8f3f67b..a31eb3a45 100644 --- a/packages/api/src/ecommerce/product/product.module.ts +++ b/packages/api/src/ecommerce/product/product.module.ts @@ -10,6 +10,8 @@ import { CoreUnification } from '@@core/@core-services/unification/core-unificat import { Utils } from '@ecommerce/@lib/@utils'; import { ShopifyService } from './services/shopify'; import { ShopifyProductMapper } from './services/shopify/mappers'; +import { WoocommerceService } from './services/woocommerce'; +import { WoocommerceProductMapper } from './services/woocommerce/mappers'; @Module({ controllers: [ProductController], @@ -22,8 +24,10 @@ import { ShopifyProductMapper } from './services/shopify/mappers'; IngestDataService, Utils, ShopifyProductMapper, + WoocommerceProductMapper, /* PROVIDERS SERVICES */ ShopifyService, + WoocommerceService, ], exports: [SyncService], }) diff --git a/packages/api/src/ecommerce/product/services/product.service.ts b/packages/api/src/ecommerce/product/services/product.service.ts index 8967b731f..ee7742c20 100644 --- a/packages/api/src/ecommerce/product/services/product.service.ts +++ b/packages/api/src/ecommerce/product/services/product.service.ts @@ -175,6 +175,7 @@ export class ProductService { return this.prisma.ecom_product_variants.create({ data: { ...email, + remote_deleted: false, id_ecom_product: existingProduct.id_ecom_product, id_connection: connection_id, }, @@ -190,6 +191,7 @@ export class ProductService { data.remote_id = product.remote_id; data.id_connection = connection_id; data.id_ecom_product = uuidv4(); + data.remote_deleted = false; const newProduct = await this.prisma.ecom_products.create({ data: data }); if (normalizedVariants && normalizedVariants.length > 0) { @@ -329,7 +331,6 @@ export class ProductService { try { let prev_cursor = null; let next_cursor = null; - if (cursor) { const isCursorPresent = await this.prisma.ecom_products.findFirst({ where: { @@ -371,71 +372,73 @@ export class ProductService { prev_cursor = Buffer.from(cursor).toString('base64'); } - const UnifiedEcommerceProducts: UnifiedEcommerceProductOutput[] = await Promise.all( - products.map(async (product) => { - // Fetch field mappings for the product - const values = await this.prisma.value.findMany({ - where: { - entity: { - ressource_owner_id: product.id_ecom_product, + const UnifiedEcommerceProducts: UnifiedEcommerceProductOutput[] = + await Promise.all( + products.map(async (product) => { + // Fetch field mappings for the product + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: product.id_ecom_product, + }, }, - }, - 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 UnifiedEcommerceProductOutput format - return { - id: product.id_ecom_product, - product_url: product.product_url, - product_type: product.product_type, - product_status: product.product_status, - images_urls: product.images_urls, - description: product.description, - vendor: product.vendor, - variants: product.ecom_product_variants.map((variant) => ({ - title: variant.title, - price: Number(variant.price), - sku: variant.sku, - options: variant.options, - weight: Number(variant.weight), - inventory_quantity: Number(variant.inventory_quantity), - })), - tags: product.tags, - field_mappings: field_mappings, - remote_id: product.remote_id, - created_at: product.created_at.toISOString(), - modified_at: product.modified_at.toISOString(), - }; - }), - ); + include: { + attribute: true, + }, + }); - let res: UnifiedEcommerceProductOutput[] = UnifiedEcommerceProducts; + // Create a map to store unique field mappings + const fieldMappingsMap = new Map(); - if (remote_data) { - const remote_array_data: UnifiedEcommerceProductOutput[] = await Promise.all( - res.map(async (product) => { - const resp = await this.prisma.remote_data.findFirst({ - where: { - ressource_owner_id: product.id, - }, + values.forEach((value) => { + fieldMappingsMap.set(value.attribute.slug, value.data); }); - const remote_data = JSON.parse(resp.data); - return { ...product, remote_data }; + + // Convert the map to an array of objects + const field_mappings = Object.fromEntries(fieldMappingsMap); + + // Transform to UnifiedEcommerceProductOutput format + return { + id: product.id_ecom_product, + product_url: product.product_url, + product_type: product.product_type, + product_status: product.product_status, + images_urls: product.images_urls, + description: product.description, + vendor: product.vendor, + variants: product.ecom_product_variants.map((variant) => ({ + title: variant.title, + price: Number(variant.price), + sku: variant.sku, + options: variant.options, + weight: Number(variant.weight), + inventory_quantity: Number(variant.inventory_quantity), + })), + tags: product.tags, + field_mappings: field_mappings, + remote_id: product.remote_id, + created_at: product.created_at.toISOString(), + modified_at: product.modified_at.toISOString(), + }; }), ); + let res: UnifiedEcommerceProductOutput[] = UnifiedEcommerceProducts; + + if (remote_data) { + const remote_array_data: UnifiedEcommerceProductOutput[] = + await Promise.all( + res.map(async (product) => { + const resp = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: product.id, + }, + }); + const remote_data = JSON.parse(resp.data); + return { ...product, remote_data }; + }), + ); + res = remote_array_data; } diff --git a/packages/api/src/ecommerce/product/services/woocommerce/index.ts b/packages/api/src/ecommerce/product/services/woocommerce/index.ts new file mode 100644 index 000000000..575fff649 --- /dev/null +++ b/packages/api/src/ecommerce/product/services/woocommerce/index.ts @@ -0,0 +1,110 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.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 { DesunifyReturnType } from '@@core/utils/types/desunify.input'; +import { SyncParam } from '@@core/utils/types/interface'; +import { OriginalProductOutput } from '@@core/utils/types/original/original.ecommerce'; +import { IProductService } from '@ecommerce/product/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { WoocommerceProductInput, WoocommerceProductOutput } from './types'; +import { EcommerceObject } from '@panora/shared'; + +@Injectable() +export class WoocommerceService implements IProductService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + EcommerceObject.product.toUpperCase() + ':' + WoocommerceService.name, + ); + this.registry.registerService('woocommerce', this); + } + + async addProduct( + productData: WoocommerceProductInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'woocommerce', + vertical: 'ecommerce', + }, + }); + const decryptedData = JSON.parse( + this.cryptoService.decrypt(connection.access_token), + ); + + const { username, password } = decryptedData; + const resp = await axios.post( + `${connection.account_url}/v3/products`, + { + product: productData, + }, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${Buffer.from( + `${username}:${password}`, + ).toString('base64')}`, + }, + }, + ); + + return { + data: resp.data, + message: 'Woocommerce product created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + async sync( + data: SyncParam, + ): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'woocommerce', + vertical: 'ecommerce', + }, + }); + const decryptedData = JSON.parse( + this.cryptoService.decrypt(connection.access_token), + ); + + const { username, password } = decryptedData; + + const resp = await axios.get(`${connection.account_url}/v3/products`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${Buffer.from( + `${username}:${password}`, + ).toString('base64')}`, + }, + }); + const products: WoocommerceProductOutput[] = resp.data; + this.logger.log(`Synced woocommerce products !`); + + return { + data: products, + message: 'Woocommerce products retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/ecommerce/product/services/woocommerce/mappers.ts b/packages/api/src/ecommerce/product/services/woocommerce/mappers.ts new file mode 100644 index 000000000..956d5ac2e --- /dev/null +++ b/packages/api/src/ecommerce/product/services/woocommerce/mappers.ts @@ -0,0 +1,148 @@ +import { + UnifiedEcommerceProductInput, + UnifiedEcommerceProductOutput, +} from '@ecommerce/product/types/model.unified'; +import { IProductMapper } from '@ecommerce/product/types'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { Injectable } from '@nestjs/common'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { Utils } from '@ecommerce/@lib/@utils'; +import { WoocommerceProductInput, WoocommerceProductOutput } from './types'; + +@Injectable() +export class WoocommerceProductMapper implements IProductMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService( + 'ecommerce', + 'product', + 'woocommerce', + this, + ); + } + + async desunify( + source: UnifiedEcommerceProductInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise> { + const res: Partial = { + name: source.product_url?.split('/').pop() || '', + description: source.description, + short_description: source.description?.substring(0, 100), + type: source.product_type as any, + status: source.product_status?.toLowerCase() as any, + featured: false, + catalog_visibility: 'visible', + sku: source.variants?.[0]?.sku, + price: source.variants?.[0]?.price.toString(), + regular_price: source.variants?.[0]?.price.toString(), + sale_price: '', + virtual: false, + downloadable: false, + manage_stock: true, + stock_quantity: source.variants?.[0]?.inventory_quantity, + stock_status: 'instock', + backorders: 'no', + sold_individually: false, + weight: source.variants?.[0]?.weight.toString(), + reviews_allowed: true, + }; + + if (source.images_urls) { + res.images = source.images_urls.map((url) => ({ src: url })) as any; + } + + if (source.tags) { + res.tags = source.tags.map((tag) => ({ name: tag })) as any; + } + + // Handle custom field mappings + if (customFieldMappings) { + res.meta_data = customFieldMappings.map((mapping) => ({ + key: mapping.slug, + value: source.field_mappings?.[mapping.remote_id] || '', + })) as any; + } + + return res; + } + + async unify( + source: WoocommerceProductOutput | WoocommerceProductOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleProductToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((product) => + this.mapSingleProductToUnified( + product, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private mapSingleProductToUnified( + product: WoocommerceProductOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): UnifiedEcommerceProductOutput { + const unified: UnifiedEcommerceProductOutput = { + remote_id: product.id.toString(), + remote_data: product, + product_url: product.permalink, + product_type: product.type, + product_status: product.status.toUpperCase(), + images_urls: product.images?.map((image) => image.src) || [], + description: product.description, + vendor: '', // WooCommerce doesn't have a direct vendor field + variants: [ + { + title: product.name, + price: parseFloat(product.price), + sku: product.sku, + options: null, + weight: parseFloat(product.weight), + inventory_quantity: product.stock_quantity || 0, + }, + ], + tags: product.tags?.map((tag) => tag.name) || [], + field_mappings: {}, + created_at: product.date_created, + modified_at: product.date_modified, + }; + + // Handle custom field mappings + if (customFieldMappings && product.meta_data) { + unified.field_mappings = product.meta_data.reduce((acc, meta) => { + const mapping = customFieldMappings.find((m) => m.slug === meta.key); + if (mapping) { + acc[mapping.remote_id] = meta.value; + } + return acc; + }, {} as Record); + } + + return unified; + } +} diff --git a/packages/api/src/ecommerce/product/services/woocommerce/types.ts b/packages/api/src/ecommerce/product/services/woocommerce/types.ts new file mode 100644 index 000000000..46bfb8928 --- /dev/null +++ b/packages/api/src/ecommerce/product/services/woocommerce/types.ts @@ -0,0 +1,109 @@ +export interface WoocommerceProductInput { + id: number; + name: string; + slug: string; + permalink: string; + date_created: string; + date_created_gmt: string; + date_modified: string; + date_modified_gmt: string; + type: 'simple' | 'grouped' | 'external' | 'variable'; + status: 'draft' | 'pending' | 'private' | 'publish'; + featured: boolean; + catalog_visibility: 'visible' | 'catalog' | 'search' | 'hidden'; + description: string; + short_description: string; + sku: string; + price: string; + regular_price: string; + sale_price: string; + date_on_sale_from: string | null; + date_on_sale_from_gmt: string | null; + date_on_sale_to: string | null; + date_on_sale_to_gmt: string | null; + price_html: string; + on_sale: boolean; + purchasable: boolean; + total_sales: number; + virtual: boolean; + downloadable: boolean; + downloads: Array<{ + id: string; + name: string; + file: string; + }>; + download_limit: number; + download_expiry: number; + external_url: string; + button_text: string; + tax_status: 'taxable' | 'shipping' | 'none'; + tax_class: string; + manage_stock: boolean; + stock_quantity: number | null; + stock_status: 'instock' | 'outofstock' | 'onbackorder'; + backorders: 'no' | 'notify' | 'yes'; + backorders_allowed: boolean; + backordered: boolean; + sold_individually: boolean; + weight: string; + dimensions: { + length: string; + width: string; + height: string; + }; + shipping_required: boolean; + shipping_taxable: boolean; + shipping_class: string; + shipping_class_id: number; + reviews_allowed: boolean; + average_rating: string; + rating_count: number; + related_ids: number[]; + upsell_ids: number[]; + cross_sell_ids: number[]; + parent_id: number; + purchase_note: string; + categories: Array<{ + id: number; + name: string; + slug: string; + }>; + tags: Array<{ + id: number; + name: string; + slug: string; + }>; + images: Array<{ + id: number; + date_created: string; + date_created_gmt: string; + date_modified: string; + date_modified_gmt: string; + src: string; + name: string; + alt: string; + }>; + attributes: Array<{ + id: number; + name: string; + position: number; + visible: boolean; + variation: boolean; + options: string[]; + }>; + default_attributes: Array<{ + id: number; + name: string; + option: string; + }>; + variations: number[]; + grouped_products: number[]; + menu_order: number; + meta_data: Array<{ + id: number; + key: string; + value: string; + }>; +} + +export type WoocommerceProductOutput = Partial; diff --git a/packages/api/src/ecommerce/product/sync/sync.service.ts b/packages/api/src/ecommerce/product/sync/sync.service.ts index ec1841560..af6d4b654 100644 --- a/packages/api/src/ecommerce/product/sync/sync.service.ts +++ b/packages/api/src/ecommerce/product/sync/sync.service.ts @@ -194,6 +194,7 @@ export class SyncService implements OnModuleInit, IBaseSync { return this.prisma.ecom_product_variants.create({ data: { ...data, + remote_deleted: false, id_ecom_product: existingProduct.id_ecom_product, id_connection: connection_id, }, @@ -210,6 +211,7 @@ export class SyncService implements OnModuleInit, IBaseSync { id_ecom_product: uuidv4(), created_at: new Date(), remote_id: originId, + remote_deleted: false, id_connection: connection_id, }, }); @@ -220,6 +222,7 @@ export class SyncService implements OnModuleInit, IBaseSync { this.prisma.ecom_product_variants.create({ data: { ...data, + remote_deleted: false, id_ecom_product: newProd.id_ecom_product, id_connection: connection_id, }, diff --git a/packages/api/src/ecommerce/product/types/index.ts b/packages/api/src/ecommerce/product/types/index.ts index ac82dc65e..e76b4d93a 100644 --- a/packages/api/src/ecommerce/product/types/index.ts +++ b/packages/api/src/ecommerce/product/types/index.ts @@ -1,5 +1,8 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { UnifiedEcommerceProductInput, UnifiedEcommerceProductOutput } from './model.unified'; +import { + UnifiedEcommerceProductInput, + UnifiedEcommerceProductOutput, +} from './model.unified'; import { OriginalProductOutput } from '@@core/utils/types/original/original.ecommerce'; import { ApiResponse } from '@@core/utils/types'; import { IBaseObjectService, SyncParam } from '@@core/utils/types/interface'; diff --git a/packages/api/src/ticketing/account/sync/sync.service.ts b/packages/api/src/ticketing/account/sync/sync.service.ts index 516af4944..fef7dc68a 100644 --- a/packages/api/src/ticketing/account/sync/sync.service.ts +++ b/packages/api/src/ticketing/account/sync/sync.service.ts @@ -48,12 +48,12 @@ export class SyncService implements OnModuleInit, IBaseSync { this.logger.log(`Syncing accounts....`); 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) { @@ -102,7 +102,9 @@ export class SyncService implements OnModuleInit, IBaseSync { this.serviceRegistry.getService(integrationId); if (!service) { - this.logger.log(`No service found in {vertical:ticketing, commonObject: account} for integration ID: ${integrationId}`); + this.logger.log( + `No service found in {vertical:ticketing, commonObject: account} for integration ID: ${integrationId}`, + ); return; } diff --git a/packages/api/swagger/swagger-spec.yaml b/packages/api/swagger/swagger-spec.yaml index c3c089254..bb601fcfc 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: @@ -189,7 +183,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -314,7 +307,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -406,7 +398,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -496,7 +487,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -632,7 +622,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -757,7 +746,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -882,7 +870,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -1006,7 +993,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -1131,7 +1117,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -1256,7 +1241,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -1348,7 +1332,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -1472,7 +1455,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -1564,7 +1546,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -1657,7 +1638,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -1785,7 +1765,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -1877,7 +1856,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -2177,15 +2155,11 @@ paths: required: false in: query schema: - minimum: 1 - default: 1 type: number - name: limit required: false in: query schema: - minimum: 1 - default: 10 type: number responses: '200': @@ -2221,12 +2195,6 @@ paths: application/json: schema: type: object - '201': - description: '' - content: - application/json: - schema: - type: object tags: &ref_21 - passthrough x-speakeasy-group: passthrough @@ -2272,7 +2240,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -2364,7 +2331,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -2456,7 +2422,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -2548,7 +2513,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -2640,7 +2604,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -2733,7 +2696,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -2857,7 +2819,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -2950,7 +2911,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -3042,7 +3002,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -3134,7 +3093,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -3226,7 +3184,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -3318,7 +3275,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -3410,7 +3366,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -3534,7 +3489,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -3626,7 +3580,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -3756,7 +3709,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -3888,7 +3840,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -4018,7 +3969,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -4148,7 +4098,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -4243,7 +4192,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -4338,7 +4286,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -4467,7 +4414,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -4562,7 +4508,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -4691,7 +4636,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -4786,7 +4730,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -4911,7 +4854,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -5036,7 +4978,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -5161,7 +5102,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -5286,7 +5226,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -5378,7 +5317,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -5503,7 +5441,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -5596,7 +5533,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -5688,7 +5624,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -5780,7 +5715,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -5872,7 +5806,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -5964,7 +5897,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -6056,7 +5988,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -6148,7 +6079,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -6240,7 +6170,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -6330,7 +6259,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -6455,7 +6383,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -6547,7 +6474,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -6673,7 +6599,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -6766,7 +6691,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -6859,7 +6783,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -6952,7 +6875,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -7077,7 +6999,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -7170,7 +7091,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -7295,7 +7215,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -7388,7 +7307,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -7513,7 +7431,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -7605,7 +7522,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -7731,7 +7647,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -7856,7 +7771,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -7949,7 +7863,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -8075,7 +7988,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -8167,7 +8079,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -8260,7 +8171,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -8353,7 +8263,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -8446,7 +8355,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -8538,7 +8446,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -8663,7 +8570,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -8788,7 +8694,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -8880,7 +8785,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -8972,7 +8876,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -9095,7 +8998,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -9218,7 +9120,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -9308,7 +9209,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -9399,7 +9299,6 @@ paths: example: 10 description: Set to get the number of records. schema: - default: 30 type: number - name: cursor required: false @@ -9530,6 +9429,7 @@ components: password_hash: type: string required: + - id_user - email - password_hash Connection: @@ -9617,8 +9517,8 @@ components: description: The unique UUID of the webhook. endpoint_description: type: string - nullable: true example: Webhook to receive connection events + nullable: true description: The description of the webhook. url: type: string @@ -9656,8 +9556,8 @@ components: last_update: format: date-time type: string - nullable: true example: '2024-10-01T12:00:00Z' + nullable: true description: The last update date of the webhook. required: - id_webhook_endpoint @@ -9692,6 +9592,7 @@ components: type: string required: - url + - description - scope SignatureVerificationDto: type: object @@ -11824,6 +11725,8 @@ components: - id_project - name - sync_mode + - pull_frequency + - redirect_url - id_user - id_connector_set CreateProjectDto: @@ -12192,10 +12095,10 @@ components: type: object properties: method: + type: string enum: - GET - POST - type: string path: type: string nullable: true @@ -12214,11 +12117,12 @@ components: type: object additionalProperties: true nullable: true - headers: - type: object required: - method - path + - data + - request_format + - overrideBaseUrl UnifiedHrisBankinfoOutput: type: object properties: {} diff --git a/packages/shared/src/connectors/metadata.ts b/packages/shared/src/connectors/metadata.ts index 427c70db1..60e0c3928 100644 --- a/packages/shared/src/connectors/metadata.ts +++ b/packages/shared/src/connectors/metadata.ts @@ -119,7 +119,8 @@ export const CONNECTORS_METADATA: ProvidersConfig = { description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, authStrategy: { - strategy: AuthStrategy.api_key + strategy: AuthStrategy.api_key, + properties: ['api_key'] } }, 'affinity': { @@ -495,7 +496,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { }, logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRqz0aID6B-InxK_03P7tCtqpXNXdawBcro67CyEE0I5g&s', description: 'Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users', - active: true, + active: false, authStrategy: { strategy: AuthStrategy.oauth2 } @@ -580,7 +581,8 @@ export const CONNECTORS_METADATA: ProvidersConfig = { description: 'Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users', active: false, authStrategy: { - strategy: AuthStrategy.api_key + strategy: AuthStrategy.api_key, + properties: ['api_key'] } }, 'freshdesk': { @@ -1033,7 +1035,8 @@ export const CONNECTORS_METADATA: ProvidersConfig = { description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, authStrategy: { - strategy: AuthStrategy.api_key + strategy: AuthStrategy.api_key, + properties: ['api_key'] } }, 'wave_financial': { @@ -1208,7 +1211,8 @@ export const CONNECTORS_METADATA: ProvidersConfig = { description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, authStrategy: { - strategy: AuthStrategy.oauth2 + strategy: AuthStrategy.api_key, + properties: ['api_key'] } }, }, @@ -1243,18 +1247,15 @@ export const CONNECTORS_METADATA: ProvidersConfig = { scopes: 'openid+email', urls: { docsUrl: 'https://documentation.bamboohr.com/docs/getting-started', - apiUrl: '', - authBaseUrl: (END_USER_DOMAIN) => `https://${END_USER_DOMAIN}.bamboohr.com/authorize.php?request=authorize` - }, - options: { - end_user_domain: true + apiUrl: (company_subdomain) => `https://api.bamboohr.com/api/gateway.php/${company_subdomain}`, }, logoPath: 'https://play-lh.googleusercontent.com/c4BW9wr_QAiIeVBYHhP7rs06w99xJzxgLvmL5I1mkucC3_ATMyL1t7Doz0_LQ0X-qS0', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, authStrategy: { - strategy: AuthStrategy.api_key - } + strategy: AuthStrategy.basic, + properties: ['username', 'company_subdomain'] + }, }, 'breezy': { scopes: '', @@ -2798,16 +2799,13 @@ export const CONNECTORS_METADATA: ProvidersConfig = { urls: { docsUrl: 'https://shopify.dev/docs/apps/build', apiUrl: (storeName: string) => `https://${storeName}.myshopify.com`, - authBaseUrl: (shop: string) => `https://${shop}.myshopify.com/admin/oauth/authorize` }, logoPath: 'https://cdn.eastsideco.com/media/v3/services/ecommerce-services/shopify-logo.png', description: 'Sync & Create orders, fulfillments, fulfillment orders, customers and products', active: false, authStrategy: { - strategy: AuthStrategy.oauth2 - }, - options: { - end_user_domain: true, + strategy: AuthStrategy.api_key, + properties: ['api_key', 'store_url'] } }, 'magento': { @@ -2826,17 +2824,14 @@ export const CONNECTORS_METADATA: ProvidersConfig = { 'woocommerce': { urls: { docsUrl: 'https://woocommerce.github.io/woocommerce-rest-api-docs/#introduction', - apiUrl: (storeName: string) => `https://${storeName}.com`, - authBaseUrl: (storeName: string) => `https://${storeName}.com/wc-auth/v1/authorize` + apiUrl: (storeName: string) => `https://${storeName}/wp-json/wc`, }, logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSHiusc7S5-BoiU1YKCztJMv_Qj7wlim4TwbA&s', description: 'Sync & Create orders, fulfillments, fulfillment orders, customers and products', - active: false, + active: true, authStrategy: { - strategy: AuthStrategy.oauth2 - }, - options: { - company_subdomain: true, + strategy: AuthStrategy.basic, + properties: ['username', 'password', 'store_url'] } }, } From 339d19a69b302db024d7ff360d4e0b51a898dd7c Mon Sep 17 00:00:00 2001 From: nael Date: Thu, 8 Aug 2024 20:52:49 +0200 Subject: [PATCH 2/2] :bug: Fix lint --- apps/magic-link/src/lib/ProviderModal.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/magic-link/src/lib/ProviderModal.tsx b/apps/magic-link/src/lib/ProviderModal.tsx index ecbd85d9d..9958cb511 100644 --- a/apps/magic-link/src/lib/ProviderModal.tsx +++ b/apps/magic-link/src/lib/ProviderModal.tsx @@ -71,8 +71,6 @@ const ProviderModal = () => { const {mutate : createApiKeyConnection} = useCreateApiKeyConnection(); const {data: magicLink} = useUniqueMagicLink(uniqueMagicLinkId); const {data: connectorsForProject} = useProjectConnectors(isProjectIdReady ? projectId : null); - - const {register,formState: {errors},handleSubmit,reset} = useForm(); const {register: register2, formState: {errors: errors2}, handleSubmit: handleSubmit2, reset: reset2} = useForm(); useEffect(() => {