From b6f5a6a388626bc5ebb89dc114d4793e1625543c Mon Sep 17 00:00:00 2001 From: nael Date: Thu, 18 Jul 2024 23:44:15 +0200 Subject: [PATCH] :bug: Added shopify oauth --- packages/api/prisma/schema.prisma | 3 +- .../unification/core-unification.service.ts | 2 +- .../api/src/@core/connections/@utils/types.ts | 16 +- .../connections/connections.controller.ts | 11 +- .../services/shopify/shopify.service.ts | 82 +- .../connections/ecommerce/types/index.ts | 1 + .../filestorage/services/box/box.service.ts | 37 +- packages/api/src/@core/utils/types/index.ts | 1 + .../types/original/original.ecommerce.ts | 25 +- packages/api/src/app.module.ts | 2 + .../api/src/ecommerce/@lib/@types/index.ts | 81 ++ .../src/ecommerce/@lib/@unification/index.ts | 1 + .../ecommerce/customer/customer.controller.ts | 25 +- .../customer/services/shopify/index.ts | 7 +- .../customer/services/shopify/types.ts | 5 +- .../ecommerce/customer/sync/sync.service.ts | 18 +- .../api/src/ecommerce/customer/types/index.ts | 5 - .../ecommerce/customer/types/model.unified.ts | 56 +- .../api/src/ecommerce/ecommerce.module.ts | 20 +- .../fulfillment/fulfillment.controller.ts | 59 +- .../fulfillment/fulfillment.module.ts | 36 +- .../fulfillment/services/ashby/mappers.ts | 73 -- .../fulfillment/services/ashby/types.ts | 8 - .../services/department.service.ts | 234 ----- .../services/fulfillment.service.ts} | 98 +- .../fulfillment/services/registry.service.ts | 10 +- .../services/shopify}/index.ts | 47 +- .../fulfillment/services/shopify/mappers.ts | 78 ++ .../fulfillment/services/shopify/types.ts | 5 + .../fulfillment/sync/sync.processor.ts | 8 +- .../fulfillment/sync/sync.service.ts | 96 +- .../src/ecommerce/fulfillment/types/index.ts | 23 +- .../fulfillment/types/model.unified.ts | 73 +- .../fulfillmentorders.controller.ts | 63 +- .../fulfillmentorders.module.ts | 36 +- .../services/ashby/mappers.ts | 73 -- .../fulfillmentorders/services/ashby/types.ts | 8 - .../services/fulfillmentorders.service.ts | 245 +++++ .../services/registry.service.ts | 10 +- .../services/{ashby => shopify}/index.ts | 50 +- .../services/shopify/mappers.ts | 85 ++ .../services/shopify/types.ts | 6 + .../fulfillmentorders/sync/sync.processor.ts | 10 +- .../fulfillmentorders/sync/sync.service.ts | 127 +-- .../fulfillmentorders/types/index.ts | 25 +- .../fulfillmentorders/types/model.unified.ts | 12 +- .../src/ecommerce/order/order.controller.ts | 59 +- .../api/src/ecommerce/order/order.module.ts | 36 +- .../ecommerce/order/services/ashby/mappers.ts | 73 -- .../ecommerce/order/services/ashby/types.ts | 8 - .../services/order.service.ts} | 98 +- .../order/services/registry.service.ts | 10 +- .../ashby => order/services/shopify}/index.ts | 45 +- .../order/services/shopify/mappers.ts | 69 ++ .../ecommerce/order/services/shopify/types.ts | 5 + .../ecommerce/order/sync/sync.processor.ts | 8 +- .../src/ecommerce/order/sync/sync.service.ts | 99 +- .../api/src/ecommerce/order/types/index.ts | 25 +- .../ecommerce/order/types/model.unified.ts | 111 ++- .../ecommerce/product/product.controller.ts | 25 +- .../product/services/product.service.ts | 19 +- .../product/services/shopify/index.ts | 1 + .../product/services/shopify/types.ts | 2 +- .../ecommerce/product/sync/sync.service.ts | 13 +- .../ecommerce/product/types/model.unified.ts | 107 ++- packages/api/swagger/swagger-spec.json | 856 ++++++++++++++++++ packages/api/swagger/swagger-spec.yaml | 555 +++++++++++- packages/shared/src/authUrl.ts | 2 +- packages/shared/src/connectors/metadata.ts | 9 +- pnpm-lock.yaml | 62 ++ 70 files changed, 3006 insertions(+), 1287 deletions(-) delete mode 100644 packages/api/src/ecommerce/fulfillment/services/ashby/mappers.ts delete mode 100644 packages/api/src/ecommerce/fulfillment/services/ashby/types.ts delete mode 100644 packages/api/src/ecommerce/fulfillment/services/department.service.ts rename packages/api/src/ecommerce/{order/services/department.service.ts => fulfillment/services/fulfillment.service.ts} (62%) rename packages/api/src/ecommerce/{order/services/ashby => fulfillment/services/shopify}/index.ts (57%) create mode 100644 packages/api/src/ecommerce/fulfillment/services/shopify/mappers.ts create mode 100644 packages/api/src/ecommerce/fulfillment/services/shopify/types.ts delete mode 100644 packages/api/src/ecommerce/fulfillmentorders/services/ashby/mappers.ts delete mode 100644 packages/api/src/ecommerce/fulfillmentorders/services/ashby/types.ts create mode 100644 packages/api/src/ecommerce/fulfillmentorders/services/fulfillmentorders.service.ts rename packages/api/src/ecommerce/fulfillmentorders/services/{ashby => shopify}/index.ts (57%) create mode 100644 packages/api/src/ecommerce/fulfillmentorders/services/shopify/mappers.ts create mode 100644 packages/api/src/ecommerce/fulfillmentorders/services/shopify/types.ts delete mode 100644 packages/api/src/ecommerce/order/services/ashby/mappers.ts delete mode 100644 packages/api/src/ecommerce/order/services/ashby/types.ts rename packages/api/src/ecommerce/{fulfillmentorders/services/department.service.ts => order/services/order.service.ts} (63%) rename packages/api/src/ecommerce/{fulfillment/services/ashby => order/services/shopify}/index.ts (61%) create mode 100644 packages/api/src/ecommerce/order/services/shopify/mappers.ts create mode 100644 packages/api/src/ecommerce/order/services/shopify/types.ts diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index 2934cd22d..510177dd0 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -933,6 +933,7 @@ model connector_sets { tcg_front Boolean? crm_zendesk Boolean? crm_close Boolean? + fs_box Boolean? projects projects[] } @@ -1218,7 +1219,7 @@ model ecom_customers { id_connection String @db.Uuid ecom_customer_addresses ecom_customer_addresses[] ecom_orders ecom_orders[] -} +} /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments model ecom_fulfilments { diff --git a/packages/api/src/@core/@core-services/unification/core-unification.service.ts b/packages/api/src/@core/@core-services/unification/core-unification.service.ts index a0ec17d50..82965b883 100644 --- a/packages/api/src/@core/@core-services/unification/core-unification.service.ts +++ b/packages/api/src/@core/@core-services/unification/core-unification.service.ts @@ -5,7 +5,7 @@ import { FileStorageObject } from '@filestorage/@lib/@types'; import { HrisObject } from '@hris/@lib/@types'; import { MarketingAutomationObject } from '@marketingautomation/@lib/@types'; import { Injectable } from '@nestjs/common'; -import { ConnectorCategory } from '@panora/shared'; +import { ConnectorCategory, EcommerceObject } from '@panora/shared'; import { TicketingObject } from '@ticketing/@lib/@types'; import { TargetObject, Unified, UnifyReturnType } from '../../utils/types'; import { DesunifyReturnType } from '../../utils/types/desunify.input'; diff --git a/packages/api/src/@core/connections/@utils/types.ts b/packages/api/src/@core/connections/@utils/types.ts index 7ca97a05b..b76f00ed9 100644 --- a/packages/api/src/@core/connections/@utils/types.ts +++ b/packages/api/src/@core/connections/@utils/types.ts @@ -1,19 +1,17 @@ -type CommonCallbackParams = { +export type OAuthCallbackParams = { projectId: string; linkedUserId: string; + code: string; + [key: string]: any; }; -export type APIKeyCallbackParams = CommonCallbackParams & { +export type APIKeyCallbackParams = { + projectId: string; + linkedUserId: string; apikey: string; body_data?: { [key: string]: any }; }; -// Define the specific callback parameters for OAUTH -export type OAuthCallbackParams = CommonCallbackParams & { - code: string; - location?: string; // for zoho -}; - // Define the discriminated union type for callback parameters export type CallbackParams = APIKeyCallbackParams | OAuthCallbackParams; @@ -38,4 +36,6 @@ export interface IConnectionCategory { id_project: string, account_url?: string, ): Promise; + + redirectUponConnection?(...params: any[]): void; } diff --git a/packages/api/src/@core/connections/connections.controller.ts b/packages/api/src/@core/connections/connections.controller.ts index 832c67759..0d64c70be 100644 --- a/packages/api/src/@core/connections/connections.controller.ts +++ b/packages/api/src/@core/connections/connections.controller.ts @@ -57,7 +57,7 @@ export class ConnectionsController { @Get('oauth/callback') async handleOAuthCallback(@Res() res: Response, @Query() query: any) { try { - const { state, code, location } = query; + const { state, code, ...otherParams } = query; if (!code) { throw new ConnectionsError({ name: 'OAUTH_CALLBACK_CODE_NOT_FOUND_ERROR', @@ -81,11 +81,16 @@ export class ConnectionsController { ); await service.handleCallBack( providerName, - { linkedUserId, projectId, code, location }, + { linkedUserId, projectId, code, otherParams }, 'oauth', ); - res.redirect(returnUrl); + if (providerName == 'shopify') { + // we must redirect using shop and host to get a valid session on shopify server + service.redirectUponConnection(res, otherParams); + } else { + res.redirect(`/`); + } /*if ( CONNECTORS_METADATA[vertical.toLowerCase()][providerName.toLowerCase()] 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 0803b5046..8bda1ebf2 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 @@ -1,50 +1,97 @@ 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 { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; import { ConnectionUtils } from '@@core/connections/@utils'; -import { APIKeyCallbackParams } from '@@core/connections/@utils/types'; +import { OAuthCallbackParams } from '@@core/connections/@utils/types'; import { Injectable } from '@nestjs/common'; -import { CONNECTORS_METADATA } from '@panora/shared'; +import { + AuthStrategy, + CONNECTORS_METADATA, + DynamicApiUrl, + OAuth2AuthData, + providerToType, +} from '@panora/shared'; import { v4 as uuidv4 } from 'uuid'; import { IEcommerceConnectionService } from '../../types'; import { ServiceRegistry } from '../registry.service'; +import axios from 'axios'; + +export type ShopifyOAuthResponse = { + access_token: string; + scope: string; +}; @Injectable() export class ShopifyConnectionService implements IEcommerceConnectionService { + private readonly type: string; + constructor( private prisma: PrismaService, private logger: LoggerService, private cryptoService: EncryptionService, private registry: ServiceRegistry, private connectionUtils: ConnectionUtils, + private cService: ConnectionsStrategiesService, ) { this.logger.setContext(ShopifyConnectionService.name); - this.registry.registerService('ashby', this); + this.registry.registerService('shopify', this); + this.type = providerToType('shopify', 'ecommerce', AuthStrategy.oauth2); } - async handleCallback(opts: APIKeyCallbackParams) { + async handleCallback(opts: OAuthCallbackParams) { try { - const { linkedUserId, projectId } = opts; + const { linkedUserId, projectId, code, hmac, shop } = opts; const isNotUnique = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, - provider_slug: 'ashby', + provider_slug: 'shopify', 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); if (isNotUnique) { db_res = await this.prisma.connections.update({ where: { id_connection: isNotUnique.id_connection, }, data: { - access_token: this.cryptoService.encrypt(opts.apikey), - account_url: CONNECTORS_METADATA['ecommerce']['ashby'].urls - .apiUrl as string, + access_token: this.cryptoService.encrypt(data.access_token), + account_url: BASE_API_URL, status: 'valid', created_at: new Date(), }, @@ -54,12 +101,11 @@ export class ShopifyConnectionService implements IEcommerceConnectionService { data: { id_connection: uuidv4(), connection_token: connection_token, - provider_slug: 'ashby', + provider_slug: 'shopify', vertical: 'ecommerce', - token_type: 'api_key', - account_url: CONNECTORS_METADATA['ecommerce']['ashby'].urls - .apiUrl as string, - access_token: this.cryptoService.encrypt(opts.apikey), + token_type: 'oauth', + account_url: BASE_API_URL, + access_token: this.cryptoService.encrypt(data.access_token), status: 'valid', created_at: new Date(), projects: { @@ -81,4 +127,10 @@ export class ShopifyConnectionService implements IEcommerceConnectionService { throw error; } } + + redirectUponConnection(...params: any[]): void { + const [{ res, host, shop }] = params; + + return res.redirect(`/?shop=${shop}&host=${encodeURIComponent(host)}`); + } } diff --git a/packages/api/src/@core/connections/ecommerce/types/index.ts b/packages/api/src/@core/connections/ecommerce/types/index.ts index 788cb2c03..0cd6b6fca 100644 --- a/packages/api/src/@core/connections/ecommerce/types/index.ts +++ b/packages/api/src/@core/connections/ecommerce/types/index.ts @@ -4,4 +4,5 @@ import { connections as Connection } from '@prisma/client'; export interface IEcommerceConnectionService { handleCallback(opts: CallbackParams): Promise; handleTokenRefresh?(opts: RefreshParams): Promise; + redirectUponConnection?(...params: any[]): void; } diff --git a/packages/api/src/@core/connections/filestorage/services/box/box.service.ts b/packages/api/src/@core/connections/filestorage/services/box/box.service.ts index eb4ff6144..244a6c83d 100644 --- a/packages/api/src/@core/connections/filestorage/services/box/box.service.ts +++ b/packages/api/src/@core/connections/filestorage/services/box/box.service.ts @@ -1,32 +1,24 @@ -import { Injectable } from '@nestjs/common'; -import axios from 'axios'; +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; +import { ConnectionUtils } from '@@core/connections/@utils'; import { - Action, - ActionType, - ConnectionsError, - format3rdPartyError, - throwTypedError, -} from '@@core/utils/errors'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; -import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; -import { IFilestorageConnectionService } from '../../types'; -import { ServiceRegistry } from '../registry.service'; + OAuthCallbackParams, + RefreshParams, +} from '@@core/connections/@utils/types'; +import { Injectable } from '@nestjs/common'; import { AuthStrategy, CONNECTORS_METADATA, OAuth2AuthData, providerToType, } from '@panora/shared'; -import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; -import { - OAuthCallbackParams, - RefreshParams, -} from '@@core/connections/@utils/types'; +import axios from 'axios'; +import { v4 as uuidv4 } from 'uuid'; +import { IFilestorageConnectionService } from '../../types'; +import { ServiceRegistry } from '../registry.service'; export type BoxOAuthResponse = { access_token: string; @@ -86,9 +78,6 @@ export class BoxConnectionService implements IFilestorageConnectionService { }, ); const data: BoxOAuthResponse = res.data; - this.logger.log( - 'OAuth credentials : box filestorage ' + JSON.stringify(data), - ); let db_res; const connection_token = uuidv4(); diff --git a/packages/api/src/@core/utils/types/index.ts b/packages/api/src/@core/utils/types/index.ts index 7232987e2..aec4b483c 100644 --- a/packages/api/src/@core/utils/types/index.ts +++ b/packages/api/src/@core/utils/types/index.ts @@ -20,6 +20,7 @@ import { UnifiedMarketingAutomation, } from '@marketingautomation/@lib/@types'; import { UnifiedEcommerce } from '@ecommerce/@lib/@types'; +import { EcommerceObject } from '@panora/shared'; export type Unified = | UnifiedCrm 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 e9608f06c..f0fa99208 100644 --- a/packages/api/src/@core/utils/types/original/original.ecommerce.ts +++ b/packages/api/src/@core/utils/types/original/original.ecommerce.ts @@ -1,25 +1,10 @@ /* INPUT */ -import { - ShopifyProductInput, - ShopifyProductOutput, -} from '@ecommerce/product/services/shopify/types'; -import { - ShopifyOrderInput, - ShopifyOrderOutput, -} from '@ecommerce/order/services/shopify/types'; -import { - ShopifyFulfillmentOrdersInput, - ShopifyFulfillmentOrdersOutput, -} from '@ecommerce/fulfillmentorders/services/shopify/types'; -import { - ShopifyCustomerInput, - ShopifyCustomerOutput, -} from '@ecommerce/customer/services/shopify/types'; -import { - ShopifyFulfillmentInput, - ShopifyFulfillmentOrderOutput, -} from '@ecommerce/fulfillment/services/shopify/types'; +import { ShopifyCustomerInput } from '@ecommerce/customer/services/shopify/types'; +import { ShopifyFulfillmentInput } from '@ecommerce/fulfillment/services/shopify/types'; +import { ShopifyFulfillmentOrdersInput } from '@ecommerce/fulfillmentorders/services/shopify/types'; +import { ShopifyOrderInput } from '@ecommerce/order/services/shopify/types'; +import { ShopifyProductInput } from '@ecommerce/product/services/shopify/types'; /* product */ export type OriginalProductInput = ShopifyProductInput; diff --git a/packages/api/src/app.module.ts b/packages/api/src/app.module.ts index 1a4eb73e7..c00dff5d1 100644 --- a/packages/api/src/app.module.ts +++ b/packages/api/src/app.module.ts @@ -16,6 +16,7 @@ import { FileStorageModule } from './filestorage/filestorage.module'; import { HrisModule } from './hris/hris.module'; import { MarketingAutomationModule } from './marketingautomation/marketingautomation.module'; import { CoreSharedModule } from '@@core/@core-services/module'; +import { EcommerceModule } from '@ecommerce/ecommerce.module'; @Module({ imports: [ @@ -26,6 +27,7 @@ import { CoreSharedModule } from '@@core/@core-services/module'; AtsModule, AccountingModule, FileStorageModule, + EcommerceModule, CrmModule, TicketingModule, ThrottlerModule.forRoot([ diff --git a/packages/api/src/ecommerce/@lib/@types/index.ts b/packages/api/src/ecommerce/@lib/@types/index.ts index 333831a6f..1cf0401ec 100644 --- a/packages/api/src/ecommerce/@lib/@types/index.ts +++ b/packages/api/src/ecommerce/@lib/@types/index.ts @@ -1,4 +1,30 @@ +import { ICustomerService } from '@ecommerce/customer/types'; +import { + UnifiedCustomerInput, + UnifiedCustomerOutput, +} from '@ecommerce/customer/types/model.unified'; +import { IFulfillmentService } from '@ecommerce/fulfillment/types'; +import { + UnifiedFulfillmentInput, + UnifiedFulfillmentOutput, +} from '@ecommerce/fulfillment/types/model.unified'; +import { IFulfillmentOrdersService } from '@ecommerce/fulfillmentorders/types'; +import { + UnifiedFulfillmentOrdersInput, + UnifiedFulfillmentOrdersOutput, +} from '@ecommerce/fulfillmentorders/types/model.unified'; +import { IOrderService } from '@ecommerce/order/types'; +import { + UnifiedOrderInput, + UnifiedOrderOutput, +} from '@ecommerce/order/types/model.unified'; import { IProductService } from '@ecommerce/product/types'; +import { + UnifiedProductInput, + UnifiedProductOutput, +} from '@ecommerce/product/types/model.unified'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsIn, IsOptional, IsString } from 'class-validator'; export type UnifiedEcommerce = | UnifiedOrderInput @@ -18,3 +44,58 @@ export type IEcommerceService = | IFulfillmentService | IFulfillmentOrdersService | ICustomerService; + +export class Address { + @ApiProperty({ + type: String, + description: 'The street', + }) + @IsString() + street_1: string; + + @ApiProperty({ + type: String, + description: 'More information about the street ', + }) + @IsString() + @IsOptional() + street_2?: string; + + @ApiProperty({ + type: String, + description: 'The city', + }) + @IsString() + city: string; + + @ApiProperty({ + type: String, + description: 'The state', + }) + @IsString() + state: string; + + @ApiProperty({ + type: String, + description: 'The postal code', + }) + @IsString() + postal_code: string; + + @ApiProperty({ + type: String, + description: 'The country', + }) + @IsString() + country: string; + + @ApiProperty({ + type: String, + description: + 'The address type. Authorized values are either PERSONAL or WORK.', + }) + @IsIn(['PERSONAL', 'WORK']) + @IsOptional() + @IsString() + address_type?: string; +} diff --git a/packages/api/src/ecommerce/@lib/@unification/index.ts b/packages/api/src/ecommerce/@lib/@unification/index.ts index d203c8ed9..a6601b3ba 100644 --- a/packages/api/src/ecommerce/@lib/@unification/index.ts +++ b/packages/api/src/ecommerce/@lib/@unification/index.ts @@ -5,6 +5,7 @@ import { IUnification } from '@@core/utils/types/interface'; import { EcommerceObjectInput } from '@@core/utils/types/original/original.ecommerce'; import { UnifySourceType } from '@@core/utils/types/unify.output'; import { Injectable } from '@nestjs/common'; +import { EcommerceObject } from '@panora/shared'; @Injectable() export class EcommerceUnificationService implements IUnification { diff --git a/packages/api/src/ecommerce/customer/customer.controller.ts b/packages/api/src/ecommerce/customer/customer.controller.ts index c54c2e4d2..7512ff953 100644 --- a/packages/api/src/ecommerce/customer/customer.controller.ts +++ b/packages/api/src/ecommerce/customer/customer.controller.ts @@ -1,32 +1,25 @@ +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +import { ApiCustomResponse } from '@@core/utils/types'; import { Controller, - Post, - Body, - Query, Get, - Patch, - Param, Headers, + Param, + Query, UseGuards, } from '@nestjs/common'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { - ApiBody, + ApiHeader, ApiOperation, ApiParam, ApiQuery, ApiTags, - ApiHeader, } from '@nestjs/swagger'; -import { ApiCustomResponse } from '@@core/utils/types'; import { CustomerService } from './services/customer.service'; -import { - UnifiedCustomerInput, - UnifiedCustomerOutput, -} from './types/model.unified'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; -import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +import { UnifiedCustomerOutput } from './types/model.unified'; @ApiTags('ecommerce/customer') @Controller('ecommerce/customer') diff --git a/packages/api/src/ecommerce/customer/services/shopify/index.ts b/packages/api/src/ecommerce/customer/services/shopify/index.ts index c74e19823..b0ec5a952 100644 --- a/packages/api/src/ecommerce/customer/services/shopify/index.ts +++ b/packages/api/src/ecommerce/customer/services/shopify/index.ts @@ -10,6 +10,7 @@ import { Injectable } from '@nestjs/common'; import axios from 'axios'; import { ServiceRegistry } from '../registry.service'; import { ShopifyCustomerOutput } from './types'; +import { EcommerceObject } from '@panora/shared'; @Injectable() export class ShopifyService implements ICustomerService { @@ -24,12 +25,6 @@ export class ShopifyService implements ICustomerService { ); this.registry.registerService('shopify', this); } - addCustomer( - customerData: DesunifyReturnType, - linkedUserId: string, - ): Promise> { - throw new Error('Method not implemented.'); - } async sync(data: SyncParam): Promise> { try { diff --git a/packages/api/src/ecommerce/customer/services/shopify/types.ts b/packages/api/src/ecommerce/customer/services/shopify/types.ts index 06402b02b..4ae9e461d 100644 --- a/packages/api/src/ecommerce/customer/services/shopify/types.ts +++ b/packages/api/src/ecommerce/customer/services/shopify/types.ts @@ -1,8 +1,5 @@ export interface ShopifyCustomerInput { - id: string; - name: string; - isArchived: boolean; - parentId: string; + [key: string]: any; } export type ShopifyCustomerOutput = Partial; diff --git a/packages/api/src/ecommerce/customer/sync/sync.service.ts b/packages/api/src/ecommerce/customer/sync/sync.service.ts index 088d36930..b76ffc7bc 100644 --- a/packages/api/src/ecommerce/customer/sync/sync.service.ts +++ b/packages/api/src/ecommerce/customer/sync/sync.service.ts @@ -10,12 +10,12 @@ import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; import { OriginalCustomerOutput } from '@@core/utils/types/original/original.ecommerce'; import { Injectable, OnModuleInit } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; -import { ATS_PROVIDERS } from '@panora/shared'; import { v4 as uuidv4 } from 'uuid'; import { ServiceRegistry } from '../services/registry.service'; import { ICustomerService } from '../types'; import { UnifiedCustomerOutput } from '../types/model.unified'; - +import { ECOMMERCE_PROVIDERS } from '@panora/shared'; +import { ecom_customers as EcommerceCustomer } from '@prisma/client'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { constructor( @@ -129,14 +129,14 @@ export class SyncService implements OnModuleInit, IBaseSync { ) => { let existingCustomer; if (!originId) { - existingCustomer = await this.prisma.ecommerce_customers.findFirst({ + existingCustomer = await this.prisma.ecom_customers.findFirst({ where: { name: customer.name, id_connection: connection_id, }, }); } else { - existingCustomer = await this.prisma.ecommerce_customers.findFirst({ + existingCustomer = await this.prisma.ecom_customers.findFirst({ where: { remote_id: originId, id_connection: connection_id, @@ -150,17 +150,17 @@ export class SyncService implements OnModuleInit, IBaseSync { }; if (existingCustomer) { - return await this.prisma.ecommerce_customers.update({ + return await this.prisma.ecom_customers.update({ where: { - id_ecommerce_customer: existingCustomer.id_ecommerce_customer, + id_ecom_customer: existingCustomer.id_ecom_customer, }, data: baseData, }); } else { - return await this.prisma.ecommerce_customers.create({ + return await this.prisma.ecom_customers.create({ data: { ...baseData, - id_ecommerce_customer: uuidv4(), + id_ecom_customer: uuidv4(), created_at: new Date(), remote_id: originId, id_connection: connection_id, @@ -174,7 +174,7 @@ export class SyncService implements OnModuleInit, IBaseSync { const originId = customer.remote_id; const res = await updateOrCreateCustomer(customer, originId); - const customer_id = res.id_ecommerce_customer; + const customer_id = res.id_ecom_customer; customers_results.push(res); // Process field mappings diff --git a/packages/api/src/ecommerce/customer/types/index.ts b/packages/api/src/ecommerce/customer/types/index.ts index 9690bbfbd..41b2f92a9 100644 --- a/packages/api/src/ecommerce/customer/types/index.ts +++ b/packages/api/src/ecommerce/customer/types/index.ts @@ -5,11 +5,6 @@ import { ApiResponse } from '@@core/utils/types'; import { IBaseObjectService, SyncParam } from '@@core/utils/types/interface'; export interface ICustomerService extends IBaseObjectService { - addCustomer( - customerData: DesunifyReturnType, - linkedUserId: string, - ): Promise>; - sync(data: SyncParam): Promise>; } diff --git a/packages/api/src/ecommerce/customer/types/model.unified.ts b/packages/api/src/ecommerce/customer/types/model.unified.ts index ce5130e9a..362c526dd 100644 --- a/packages/api/src/ecommerce/customer/types/model.unified.ts +++ b/packages/api/src/ecommerce/customer/types/model.unified.ts @@ -1,17 +1,55 @@ +import { Address } from '@ecommerce/@lib/@types'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsUUID, IsOptional, IsString, IsDateString } from 'class-validator'; +import { + IsUUID, + IsOptional, + IsString, + IsDateString, + IsEmail, +} from 'class-validator'; export class UnifiedCustomerInput { @ApiPropertyOptional({ type: String, - description: 'The name of the customer', + description: 'The email of the customer', + }) + @IsEmail() + @IsOptional() + email?: string; + + @ApiPropertyOptional({ + type: String, + description: 'The first name of the customer', }) @IsString() @IsOptional() - name?: string; + first_name?: string; @ApiPropertyOptional({ - type: {}, + type: String, + description: 'The last name of the customer', + }) + @IsString() + @IsOptional() + last_name?: string; + + @ApiPropertyOptional({ + type: String, + description: 'The phone number of the customer', + }) + @IsString() + @IsOptional() + phone_number?: string; + + @ApiPropertyOptional({ + type: [Object], + description: 'The addresses of the customer', + }) + @IsOptional() + addresses?: Address[]; + + @ApiPropertyOptional({ + type: String, description: 'The custom field mappings of the object between the remote 3rd party & Panora', }) @@ -46,16 +84,18 @@ export class UnifiedCustomerOutput extends UnifiedCustomerInput { remote_data?: Record; @ApiPropertyOptional({ - type: {}, + type: String, description: 'The created date of the object', }) + @IsDateString() @IsOptional() - created_at?: any; + created_at?: string; @ApiPropertyOptional({ - type: {}, + type: String, description: 'The modified date of the object', }) + @IsDateString() @IsOptional() - modified_at?: any; + modified_at?: string; } diff --git a/packages/api/src/ecommerce/ecommerce.module.ts b/packages/api/src/ecommerce/ecommerce.module.ts index a65b8e0d4..57d5c232a 100644 --- a/packages/api/src/ecommerce/ecommerce.module.ts +++ b/packages/api/src/ecommerce/ecommerce.module.ts @@ -1,10 +1,26 @@ import { Module } from '@nestjs/common'; import { EcommerceUnificationService } from './@lib/@unification'; import { ProductModule } from './product/product.module'; +import { OrderModule } from './order/order.module'; +import { FulfillmentModule } from './fulfillment/fulfillment.module'; +import { FulfillmentOrdersModule } from './fulfillmentorders/fulfillmentorders.module'; +import { CustomerModule } from './customer/customer.module'; @Module({ - exports: [ProductModule], + exports: [ + ProductModule, + OrderModule, + FulfillmentModule, + FulfillmentOrdersModule, + CustomerModule, + ], providers: [EcommerceUnificationService], - imports: [ProductModule], + imports: [ + ProductModule, + OrderModule, + CustomerModule, + FulfillmentModule, + FulfillmentOrdersModule, + ], }) export class EcommerceModule {} diff --git a/packages/api/src/ecommerce/fulfillment/fulfillment.controller.ts b/packages/api/src/ecommerce/fulfillment/fulfillment.controller.ts index 18f984923..30556aa75 100644 --- a/packages/api/src/ecommerce/fulfillment/fulfillment.controller.ts +++ b/packages/api/src/ecommerce/fulfillment/fulfillment.controller.ts @@ -1,47 +1,40 @@ +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +import { ApiCustomResponse } from '@@core/utils/types'; import { Controller, - Post, - Body, - Query, Get, - Patch, - Param, Headers, + Param, + Query, UseGuards, } from '@nestjs/common'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { - ApiBody, + ApiHeader, ApiOperation, ApiParam, ApiQuery, ApiTags, - ApiHeader, } from '@nestjs/swagger'; -import { ApiCustomResponse } from '@@core/utils/types'; -import { DepartmentService } from './services/department.service'; -import { - UnifiedDepartmentInput, - UnifiedDepartmentOutput, -} from './types/model.unified'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; -import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +import { FulfillmentService } from './services/fulfillment.service'; +import { UnifiedFulfillmentOutput } from './types/model.unified'; -@ApiTags('ats/department') -@Controller('ats/department') -export class DepartmentController { +@ApiTags('ecommerce/fulfillment') +@Controller('ecommerce/fulfillment') +export class FulfillmentController { constructor( - private readonly departmentService: DepartmentService, + private readonly fulfillmentService: FulfillmentService, private logger: LoggerService, private connectionUtils: ConnectionUtils, ) { - this.logger.setContext(DepartmentController.name); + this.logger.setContext(FulfillmentController.name); } @ApiOperation({ - operationId: 'getDepartments', - summary: 'List a batch of Departments', + operationId: 'getFulfillments', + summary: 'List a batch of Fulfillments', }) @ApiHeader({ name: 'x-connection-token', @@ -49,10 +42,10 @@ export class DepartmentController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiCustomResponse(UnifiedDepartmentOutput) + @ApiCustomResponse(UnifiedFulfillmentOutput) @UseGuards(ApiKeyAuthGuard) @Get() - async getDepartments( + async getFulfillments( @Headers('x-connection-token') connection_token: string, @Query() query: FetchObjectsQueryDto, ) { @@ -62,7 +55,7 @@ export class DepartmentController { connection_token, ); const { remote_data, limit, cursor } = query; - return this.departmentService.getDepartments( + return this.fulfillmentService.getFulfillments( connectionId, remoteSource, linkedUserId, @@ -76,15 +69,15 @@ export class DepartmentController { } @ApiOperation({ - operationId: 'getDepartment', - summary: 'Retrieve a Department', - description: 'Retrieve a department from any connected Ats software', + operationId: 'getFulfillment', + summary: 'Retrieve a Fulfillment', + description: 'Retrieve a fulfillment from any connected Ats software', }) @ApiParam({ name: 'id', required: true, type: String, - description: 'id of the department you want to retrieve.', + description: 'id of the fulfillment you want to retrieve.', }) @ApiQuery({ name: 'remote_data', @@ -98,7 +91,7 @@ export class DepartmentController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiCustomResponse(UnifiedDepartmentOutput) + @ApiCustomResponse(UnifiedFulfillmentOutput) @UseGuards(ApiKeyAuthGuard) @Get(':id') async retrieve( @@ -110,7 +103,7 @@ export class DepartmentController { await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); - return this.departmentService.getDepartment( + return this.fulfillmentService.getFulfillment( id, linkedUserId, remoteSource, diff --git a/packages/api/src/ecommerce/fulfillment/fulfillment.module.ts b/packages/api/src/ecommerce/fulfillment/fulfillment.module.ts index 7d939d74a..55d384d37 100644 --- a/packages/api/src/ecommerce/fulfillment/fulfillment.module.ts +++ b/packages/api/src/ecommerce/fulfillment/fulfillment.module.ts @@ -1,41 +1,31 @@ -import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; +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 { ConnectionUtils } from '@@core/connections/@utils'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Utils } from '@ecommerce/@lib/@utils'; import { Module } from '@nestjs/common'; -import { DepartmentController } from './fulfillment.controller'; -import { DepartmentService } from './services/department.service'; +import { FulfillmentController } from './fulfillment.controller'; +import { FulfillmentService } from './services/fulfillment.service'; import { ServiceRegistry } from './services/registry.service'; +import { ShopifyService } from './services/shopify'; +import { ShopifyFulfillmentMapper } from './services/shopify/mappers'; import { SyncService } from './sync/sync.service'; -import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { AshbyService } from './services/ashby'; - -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; - -import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; -import { AshbyDepartmentMapper } from './services/ashby/mappers'; -import { Utils } from '@ats/@lib/@utils'; @Module({ imports: [BullQueueModule], - controllers: [DepartmentController], + controllers: [FulfillmentController], providers: [ - DepartmentService, + FulfillmentService, CoreUnification, - SyncService, WebhookService, - ServiceRegistry, - IngestDataService, - Utils, - AshbyDepartmentMapper, + ShopifyFulfillmentMapper, /* PROVIDERS SERVICES */ - AshbyService, + ShopifyService, ], exports: [SyncService], }) -export class DepartmentModule {} +export class FulfillmentModule {} diff --git a/packages/api/src/ecommerce/fulfillment/services/ashby/mappers.ts b/packages/api/src/ecommerce/fulfillment/services/ashby/mappers.ts deleted file mode 100644 index 877efdf88..000000000 --- a/packages/api/src/ecommerce/fulfillment/services/ashby/mappers.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { AshbyDepartmentInput, AshbyDepartmentOutput } from './types'; -import { - UnifiedDepartmentInput, - UnifiedDepartmentOutput, -} from '@ats/department/types/model.unified'; -import { IDepartmentMapper } from '@ats/department/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 '@ats/@lib/@utils'; - -@Injectable() -export class AshbyDepartmentMapper implements IDepartmentMapper { - constructor( - private mappersRegistry: MappersRegistry, - private utils: Utils, - private coreUnificationService: CoreUnification, - ) { - this.mappersRegistry.registerService('ats', 'department', 'ashby', this); - } - - async desunify( - source: UnifiedDepartmentInput, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): Promise { - return; - } - - async unify( - source: AshbyDepartmentOutput | AshbyDepartmentOutput[], - connectionId: string, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): Promise { - if (!Array.isArray(source)) { - return await this.mapSingleDepartmentToUnified( - source, - connectionId, - customFieldMappings, - ); - } - // Handling array of AshbyDepartmentOutput - return Promise.all( - source.map((department) => - this.mapSingleDepartmentToUnified( - department, - connectionId, - customFieldMappings, - ), - ), - ); - } - - private async mapSingleDepartmentToUnified( - department: AshbyDepartmentOutput, - connectionId: string, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): Promise { - return { - remote_id: department.id, - remote_data: department, - name: department.name || null, - }; - } -} diff --git a/packages/api/src/ecommerce/fulfillment/services/ashby/types.ts b/packages/api/src/ecommerce/fulfillment/services/ashby/types.ts deleted file mode 100644 index d3c47509a..000000000 --- a/packages/api/src/ecommerce/fulfillment/services/ashby/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface AshbyDepartmentInput { - id: string; - name: string; - isArchived: boolean; - parentId: string; -} - -export type AshbyDepartmentOutput = Partial; diff --git a/packages/api/src/ecommerce/fulfillment/services/department.service.ts b/packages/api/src/ecommerce/fulfillment/services/department.service.ts deleted file mode 100644 index deb50851d..000000000 --- a/packages/api/src/ecommerce/fulfillment/services/department.service.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { v4 as uuidv4 } from 'uuid'; -import { UnifiedDepartmentOutput } from '../types/model.unified'; - -@Injectable() -export class DepartmentService { - constructor(private prisma: PrismaService, private logger: LoggerService) { - this.logger.setContext(DepartmentService.name); - } - - async getDepartment( - id_ats_department: string, - linkedUserId: string, - integrationId: string, - remote_data?: boolean, - ): Promise { - try { - const department = await this.prisma.ats_departments.findUnique({ - where: { - id_ats_department: id_ats_department, - }, - }); - - if (!department) { - throw new Error(`Department with ID ${id_ats_department} not found.`); - } - - // Fetch field mappings for the department - const values = await this.prisma.value.findMany({ - where: { - entity: { - ressource_owner_id: department.id_ats_department, - }, - }, - 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 = Array.from(fieldMappingsMap, ([key, value]) => ({ - [key]: value, - })); - - // Transform to UnifiedDepartmentOutput format - const unifiedDepartment: UnifiedDepartmentOutput = { - id: department.id_ats_department, - name: department.name, - field_mappings: field_mappings, - remote_id: department.remote_id, - created_at: department.created_at, - modified_at: department.modified_at, - }; - - let res: UnifiedDepartmentOutput = unifiedDepartment; - if (remote_data) { - const resp = await this.prisma.remote_data.findFirst({ - where: { - ressource_owner_id: department.id_ats_department, - }, - }); - const remote_data = JSON.parse(resp.data); - - res = { - ...res, - remote_data: remote_data, - }; - } - await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'success', - type: 'ats.department.pull', - method: 'GET', - url: '/ats/department', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - - return res; - } catch (error) { - throw error; - } - } - - async getDepartments( - connection_id: string, - integrationId: string, - linkedUserId: string, - limit: number, - remote_data?: boolean, - cursor?: string, - ): Promise<{ - data: UnifiedDepartmentOutput[]; - prev_cursor: null | string; - next_cursor: null | string; - }> { - try { - let prev_cursor = null; - let next_cursor = null; - - if (cursor) { - const isCursorPresent = await this.prisma.ats_departments.findFirst({ - where: { - id_connection: connection_id, - id_ats_department: cursor, - }, - }); - if (!isCursorPresent) { - throw new ReferenceError(`The provided cursor does not exist!`); - } - } - - const departments = await this.prisma.ats_departments.findMany({ - take: limit + 1, - cursor: cursor - ? { - id_ats_department: cursor, - } - : undefined, - orderBy: { - created_at: 'asc', - }, - where: { - id_connection: connection_id, - }, - }); - - if (departments.length === limit + 1) { - next_cursor = Buffer.from( - departments[departments.length - 1].id_ats_department, - ).toString('base64'); - departments.pop(); - } - - if (cursor) { - prev_cursor = Buffer.from(cursor).toString('base64'); - } - - const unifiedDepartments: UnifiedDepartmentOutput[] = await Promise.all( - departments.map(async (department) => { - // Fetch field mappings for the department - const values = await this.prisma.value.findMany({ - where: { - entity: { - ressource_owner_id: department.id_ats_department, - }, - }, - 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 = Array.from( - fieldMappingsMap, - ([key, value]) => ({ - [key]: value, - }), - ); - - // Transform to UnifiedDepartmentOutput format - return { - id: department.id_ats_department, - name: department.name, - field_mappings: field_mappings, - remote_id: department.remote_id, - created_at: department.created_at, - modified_at: department.modified_at, - }; - }), - ); - - let res: UnifiedDepartmentOutput[] = unifiedDepartments; - - if (remote_data) { - const remote_array_data: UnifiedDepartmentOutput[] = await Promise.all( - res.map(async (department) => { - const resp = await this.prisma.remote_data.findFirst({ - where: { - ressource_owner_id: department.id, - }, - }); - const remote_data = JSON.parse(resp.data); - return { ...department, remote_data }; - }), - ); - - res = remote_array_data; - } - - await this.prisma.events.create({ - data: { - id_event: uuidv4(), - status: 'success', - type: 'ats.department.pull', - method: 'GET', - url: '/ats/departments', - provider: integrationId, - direction: '0', - timestamp: new Date(), - id_linked_user: linkedUserId, - }, - }); - - return { - data: res, - prev_cursor, - next_cursor, - }; - } catch (error) { - throw error; - } - } -} diff --git a/packages/api/src/ecommerce/order/services/department.service.ts b/packages/api/src/ecommerce/fulfillment/services/fulfillment.service.ts similarity index 62% rename from packages/api/src/ecommerce/order/services/department.service.ts rename to packages/api/src/ecommerce/fulfillment/services/fulfillment.service.ts index deb50851d..170c9f95c 100644 --- a/packages/api/src/ecommerce/order/services/department.service.ts +++ b/packages/api/src/ecommerce/fulfillment/services/fulfillment.service.ts @@ -2,36 +2,36 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { UnifiedDepartmentOutput } from '../types/model.unified'; +import { UnifiedFulfillmentOutput } from '../types/model.unified'; @Injectable() -export class DepartmentService { +export class FulfillmentService { constructor(private prisma: PrismaService, private logger: LoggerService) { - this.logger.setContext(DepartmentService.name); + this.logger.setContext(FulfillmentService.name); } - async getDepartment( - id_ats_department: string, + async getFulfillment( + id_ecom_fulfilment: string, linkedUserId: string, integrationId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { - const department = await this.prisma.ats_departments.findUnique({ + const fulfillment = await this.prisma.ecom_fulfilments.findUnique({ where: { - id_ats_department: id_ats_department, + id_ecom_fulfilment: id_ecom_fulfilment, }, }); - if (!department) { - throw new Error(`Department with ID ${id_ats_department} not found.`); + if (!fulfillment) { + throw new Error(`Fulfillment with ID ${id_ecom_fulfilment} not found.`); } - // Fetch field mappings for the department + // Fetch field mappings for the fulfillment const values = await this.prisma.value.findMany({ where: { entity: { - ressource_owner_id: department.id_ats_department, + ressource_owner_id: fulfillment.id_ecom_fulfilment, }, }, include: { @@ -51,21 +51,21 @@ export class DepartmentService { [key]: value, })); - // Transform to UnifiedDepartmentOutput format - const unifiedDepartment: UnifiedDepartmentOutput = { - id: department.id_ats_department, - name: department.name, + // Transform to UnifiedFulfillmentOutput format + const unifiedFulfillment: UnifiedFulfillmentOutput = { + id: fulfillment.id_ecom_fulfilment, + name: fulfillment.name, field_mappings: field_mappings, - remote_id: department.remote_id, - created_at: department.created_at, - modified_at: department.modified_at, + remote_id: fulfillment.remote_id, + created_at: fulfillment.created_at, + modified_at: fulfillment.modified_at, }; - let res: UnifiedDepartmentOutput = unifiedDepartment; + let res: UnifiedFulfillmentOutput = unifiedFulfillment; if (remote_data) { const resp = await this.prisma.remote_data.findFirst({ where: { - ressource_owner_id: department.id_ats_department, + ressource_owner_id: fulfillment.id_ecom_fulfilment, }, }); const remote_data = JSON.parse(resp.data); @@ -79,9 +79,9 @@ export class DepartmentService { data: { id_event: uuidv4(), status: 'success', - type: 'ats.department.pull', + type: 'ecommerce.fulfillment.pull', method: 'GET', - url: '/ats/department', + url: '/ecommerce/fulfillment', provider: integrationId, direction: '0', timestamp: new Date(), @@ -95,7 +95,7 @@ export class DepartmentService { } } - async getDepartments( + async getFulfillments( connection_id: string, integrationId: string, linkedUserId: string, @@ -103,7 +103,7 @@ export class DepartmentService { remote_data?: boolean, cursor?: string, ): Promise<{ - data: UnifiedDepartmentOutput[]; + data: UnifiedFulfillmentOutput[]; prev_cursor: null | string; next_cursor: null | string; }> { @@ -112,10 +112,10 @@ export class DepartmentService { let next_cursor = null; if (cursor) { - const isCursorPresent = await this.prisma.ats_departments.findFirst({ + const isCursorPresent = await this.prisma.ecom_fulfilments.findFirst({ where: { id_connection: connection_id, - id_ats_department: cursor, + id_ecom_fulfilment: cursor, }, }); if (!isCursorPresent) { @@ -123,11 +123,11 @@ export class DepartmentService { } } - const departments = await this.prisma.ats_departments.findMany({ + const fulfillments = await this.prisma.ecom_fulfilments.findMany({ take: limit + 1, cursor: cursor ? { - id_ats_department: cursor, + id_ecom_fulfilment: cursor, } : undefined, orderBy: { @@ -138,24 +138,24 @@ export class DepartmentService { }, }); - if (departments.length === limit + 1) { + if (fulfillments.length === limit + 1) { next_cursor = Buffer.from( - departments[departments.length - 1].id_ats_department, + fulfillments[fulfillments.length - 1].id_ecom_fulfilment, ).toString('base64'); - departments.pop(); + fulfillments.pop(); } if (cursor) { prev_cursor = Buffer.from(cursor).toString('base64'); } - const unifiedDepartments: UnifiedDepartmentOutput[] = await Promise.all( - departments.map(async (department) => { - // Fetch field mappings for the department + const unifiedFulfillments: UnifiedFulfillmentOutput[] = await Promise.all( + fulfillments.map(async (fulfillment) => { + // Fetch field mappings for the fulfillment const values = await this.prisma.value.findMany({ where: { entity: { - ressource_owner_id: department.id_ats_department, + ressource_owner_id: fulfillment.id_ecom_fulfilment, }, }, include: { @@ -178,30 +178,30 @@ export class DepartmentService { }), ); - // Transform to UnifiedDepartmentOutput format + // Transform to UnifiedFulfillmentOutput format return { - id: department.id_ats_department, - name: department.name, + id: fulfillment.id_ecom_fulfilment, + name: fulfillment.name, field_mappings: field_mappings, - remote_id: department.remote_id, - created_at: department.created_at, - modified_at: department.modified_at, + remote_id: fulfillment.remote_id, + created_at: fulfillment.created_at, + modified_at: fulfillment.modified_at, }; }), ); - let res: UnifiedDepartmentOutput[] = unifiedDepartments; + let res: UnifiedFulfillmentOutput[] = unifiedFulfillments; if (remote_data) { - const remote_array_data: UnifiedDepartmentOutput[] = await Promise.all( - res.map(async (department) => { + const remote_array_data: UnifiedFulfillmentOutput[] = await Promise.all( + res.map(async (fulfillment) => { const resp = await this.prisma.remote_data.findFirst({ where: { - ressource_owner_id: department.id, + ressource_owner_id: fulfillment.id, }, }); const remote_data = JSON.parse(resp.data); - return { ...department, remote_data }; + return { ...fulfillment, remote_data }; }), ); @@ -212,9 +212,9 @@ export class DepartmentService { data: { id_event: uuidv4(), status: 'success', - type: 'ats.department.pull', + type: 'ecommerce.fulfillment.pull', method: 'GET', - url: '/ats/departments', + url: '/ecommerce/fulfillments', provider: integrationId, direction: '0', timestamp: new Date(), diff --git a/packages/api/src/ecommerce/fulfillment/services/registry.service.ts b/packages/api/src/ecommerce/fulfillment/services/registry.service.ts index ee946e7b7..3ae403d6c 100644 --- a/packages/api/src/ecommerce/fulfillment/services/registry.service.ts +++ b/packages/api/src/ecommerce/fulfillment/services/registry.service.ts @@ -1,19 +1,19 @@ import { Injectable } from '@nestjs/common'; -import { IDepartmentService } from '../types'; +import { IFulfillmentService } from '../types'; @Injectable() export class ServiceRegistry { - private serviceMap: Map; + private serviceMap: Map; constructor() { - this.serviceMap = new Map(); + this.serviceMap = new Map(); } - registerService(serviceKey: string, service: IDepartmentService) { + registerService(serviceKey: string, service: IFulfillmentService) { this.serviceMap.set(serviceKey, service); } - getService(integrationId: string): IDepartmentService { + getService(integrationId: string): IFulfillmentService { const service = this.serviceMap.get(integrationId); if (!service) { throw new ReferenceError( diff --git a/packages/api/src/ecommerce/order/services/ashby/index.ts b/packages/api/src/ecommerce/fulfillment/services/shopify/index.ts similarity index 57% rename from packages/api/src/ecommerce/order/services/ashby/index.ts rename to packages/api/src/ecommerce/fulfillment/services/shopify/index.ts index cceda214e..7c60f4cfd 100644 --- a/packages/api/src/ecommerce/order/services/ashby/index.ts +++ b/packages/api/src/ecommerce/fulfillment/services/shopify/index.ts @@ -1,20 +1,19 @@ -import { Injectable } from '@nestjs/common'; -import { IDepartmentService } from '@ats/department/types'; -import { AtsObject } from '@ats/@lib/@types'; -import axios from 'axios'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { ActionType, handle3rdPartyServiceError } from '@@core/utils/errors'; 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 { ServiceRegistry } from '../registry.service'; -import { AshbyDepartmentInput, AshbyDepartmentOutput } from './types'; import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { OriginalDepartmentOutput } from '@@core/utils/types/original/original.ats'; import { SyncParam } from '@@core/utils/types/interface'; +import { OriginalFulfillmentOutput } from '@@core/utils/types/original/original.ecommerce'; +import { IFulfillmentService } from '@ecommerce/fulfillment/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { ShopifyFulfillmentOutput } from './types'; +import { EcommerceObject } from '@panora/shared'; @Injectable() -export class AshbyService implements IDepartmentService { +export class ShopifyService implements IFulfillmentService { constructor( private prisma: PrismaService, private logger: LoggerService, @@ -22,26 +21,22 @@ export class AshbyService implements IDepartmentService { private registry: ServiceRegistry, ) { this.logger.setContext( - AtsObject.department.toUpperCase() + ':' + AshbyService.name, + EcommerceObject.fulfillment.toUpperCase() + ':' + ShopifyService.name, ); - this.registry.registerService('ashby', this); - } - addDepartment( - departmentData: DesunifyReturnType, - linkedUserId: string, - ): Promise> { - throw new Error('Method not implemented.'); + this.registry.registerService('shopify', this); } - async sync(data: SyncParam): Promise> { + async sync( + data: SyncParam, + ): Promise> { try { const { linkedUserId } = data; const connection = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, - provider_slug: 'ashby', - vertical: 'ats', + provider_slug: 'shopify', + vertical: 'ecommerce', }, }); const resp = await axios.post( @@ -55,12 +50,12 @@ export class AshbyService implements IDepartmentService { }, }, ); - const departments: AshbyDepartmentOutput[] = resp.data.results; - this.logger.log(`Synced ashby departments !`); + const fulfillments: ShopifyFulfillmentOutput[] = resp.data.results; + this.logger.log(`Synced shopify fulfillments !`); return { - data: departments, - message: 'Ashby departments retrieved', + data: fulfillments, + message: 'Shopify fulfillments retrieved', statusCode: 200, }; } catch (error) { diff --git a/packages/api/src/ecommerce/fulfillment/services/shopify/mappers.ts b/packages/api/src/ecommerce/fulfillment/services/shopify/mappers.ts new file mode 100644 index 000000000..50c6c7cb7 --- /dev/null +++ b/packages/api/src/ecommerce/fulfillment/services/shopify/mappers.ts @@ -0,0 +1,78 @@ +import { ShopifyFulfillmentInput, ShopifyFulfillmentOutput } from './types'; +import { + UnifiedFulfillmentInput, + UnifiedFulfillmentOutput, +} from '@ecommerce/fulfillment/types/model.unified'; +import { IFulfillmentMapper } from '@ecommerce/fulfillment/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 ShopifyFulfillmentMapper implements IFulfillmentMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService( + 'ecommerce', + 'fulfillment', + 'shopify', + this, + ); + } + + async desunify( + source: UnifiedFulfillmentInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + return; + } + + async unify( + source: ShopifyFulfillmentOutput | ShopifyFulfillmentOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return await this.mapSingleFulfillmentToUnified( + source, + connectionId, + customFieldMappings, + ); + } + // Handling array of ShopifyFulfillmentOutput + return Promise.all( + source.map((fulfillment) => + this.mapSingleFulfillmentToUnified( + fulfillment, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleFulfillmentToUnified( + fulfillment: ShopifyFulfillmentOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + return { + remote_id: fulfillment.id, + remote_data: fulfillment, + name: fulfillment.name || null, + }; + } +} diff --git a/packages/api/src/ecommerce/fulfillment/services/shopify/types.ts b/packages/api/src/ecommerce/fulfillment/services/shopify/types.ts new file mode 100644 index 000000000..46d8a9294 --- /dev/null +++ b/packages/api/src/ecommerce/fulfillment/services/shopify/types.ts @@ -0,0 +1,5 @@ +export interface ShopifyFulfillmentInput { + [key: string]: any; +} + +export type ShopifyFulfillmentOutput = Partial; diff --git a/packages/api/src/ecommerce/fulfillment/sync/sync.processor.ts b/packages/api/src/ecommerce/fulfillment/sync/sync.processor.ts index 4c01a02f0..2c0ecfdc7 100644 --- a/packages/api/src/ecommerce/fulfillment/sync/sync.processor.ts +++ b/packages/api/src/ecommerce/fulfillment/sync/sync.processor.ts @@ -7,13 +7,13 @@ import { Queues } from '@@core/@core-services/queues/types'; export class SyncProcessor { constructor(private syncService: SyncService) {} - @Process('ats-sync-departments') - async handleSyncDepartments(job: Job) { + @Process('ecommerce-sync-fulfillments') + async handleSyncFulfillments(job: Job) { try { - console.log(`Processing queue -> ats-sync-departments ${job.id}`); + console.log(`Processing queue -> ecommerce-sync-fulfillments ${job.id}`); await this.syncService.kickstartSync(); } catch (error) { - console.error('Error syncing ats departments', error); + console.error('Error syncing ecommerce fulfillments', error); } } } diff --git a/packages/api/src/ecommerce/fulfillment/sync/sync.service.ts b/packages/api/src/ecommerce/fulfillment/sync/sync.service.ts index 61c7cbe95..138e0905c 100644 --- a/packages/api/src/ecommerce/fulfillment/sync/sync.service.ts +++ b/packages/api/src/ecommerce/fulfillment/sync/sync.service.ts @@ -1,23 +1,21 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { BullQueueService } from '@@core/@core-services/queues/shared.service'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { OriginalFulfillmentOutput } from '@@core/utils/types/original/original.ecommerce'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; +import { ECOMMERCE_PROVIDERS } from '@panora/shared'; +import { ecom_fulfilments as EcommerceFulfillment } from '@prisma/client'; import { v4 as uuidv4 } from 'uuid'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; -import { ApiResponse } from '@@core/utils/types'; -import { IDepartmentService } from '../types'; -import { OriginalDepartmentOutput } from '@@core/utils/types/original/original.ats'; -import { UnifiedDepartmentOutput } from '../types/model.unified'; -import { ats_departments as AtsDepartment } from '@prisma/client'; -import { ATS_PROVIDERS } from '@panora/shared'; -import { AtsObject } from '@ats/@lib/@types'; -import { BullQueueService } from '@@core/@core-services/queues/shared.service'; -import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; -import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IFulfillmentService } from '../types'; +import { UnifiedFulfillmentOutput } from '../types/model.unified'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -33,13 +31,13 @@ export class SyncService implements OnModuleInit, IBaseSync { private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); - this.registry.registerService('ats', 'department', this); + this.registry.registerService('ecommerce', 'fulfillment', this); } async onModuleInit() { try { await this.bullQueueService.queueSyncJob( - 'ats-sync-departments', + 'ecommerce-sync-fulfillments', '0 0 * * *', ); } catch (error) { @@ -50,7 +48,7 @@ export class SyncService implements OnModuleInit, IBaseSync { @Cron('0 */8 * * *') // every 8 hours async kickstartSync(user_id?: string) { try { - this.logger.log('Syncing departments...'); + this.logger.log('Syncing fulfillments...'); const users = user_id ? [ await this.prisma.users.findUnique({ @@ -76,7 +74,7 @@ export class SyncService implements OnModuleInit, IBaseSync { }); linkedUsers.map(async (linkedUser) => { try { - const providers = ATS_PROVIDERS; + const providers = ECOMMERCE_PROVIDERS; for (const provider of providers) { try { await this.syncForLinkedUser({ @@ -102,15 +100,15 @@ export class SyncService implements OnModuleInit, IBaseSync { async syncForLinkedUser(param: SyncLinkedUserType) { try { const { integrationId, linkedUserId } = param; - const service: IDepartmentService = + const service: IFulfillmentService = this.serviceRegistry.getService(integrationId); if (!service) return; await this.ingestService.syncForLinkedUser< - UnifiedDepartmentOutput, - OriginalDepartmentOutput, - IDepartmentService - >(integrationId, linkedUserId, 'ats', 'department', service, []); + UnifiedFulfillmentOutput, + OriginalFulfillmentOutput, + IFulfillmentService + >(integrationId, linkedUserId, 'ecommerce', 'fulfillment', service, []); } catch (error) { throw error; } @@ -119,27 +117,27 @@ export class SyncService implements OnModuleInit, IBaseSync { async saveToDb( connection_id: string, linkedUserId: string, - departments: UnifiedDepartmentOutput[], + fulfillments: UnifiedFulfillmentOutput[], originSource: string, remote_data: Record[], - ): Promise { + ): Promise { try { - const departments_results: AtsDepartment[] = []; + const fulfillments_results: EcommerceFulfillment[] = []; - const updateOrCreateDepartment = async ( - department: UnifiedDepartmentOutput, + const updateOrCreateFulfillment = async ( + fulfillment: UnifiedFulfillmentOutput, originId: string, ) => { - let existingDepartment; + let existingFulfillment; if (!originId) { - existingDepartment = await this.prisma.ats_departments.findFirst({ + existingFulfillment = await this.prisma.ecom_fulfilments.findFirst({ where: { - name: department.name, + name: fulfillment.name, id_connection: connection_id, }, }); } else { - existingDepartment = await this.prisma.ats_departments.findFirst({ + existingFulfillment = await this.prisma.ecom_fulfilments.findFirst({ where: { remote_id: originId, id_connection: connection_id, @@ -148,22 +146,22 @@ export class SyncService implements OnModuleInit, IBaseSync { } const baseData: any = { - name: department.name ?? null, + name: fulfillment.name ?? null, modified_at: new Date(), }; - if (existingDepartment) { - return await this.prisma.ats_departments.update({ + if (existingFulfillment) { + return await this.prisma.ecom_fulfilments.update({ where: { - id_ats_department: existingDepartment.id_ats_department, + id_ecom_fulfilment: existingFulfillment.id_ecom_fulfilment, }, data: baseData, }); } else { - return await this.prisma.ats_departments.create({ + return await this.prisma.ecom_fulfilments.create({ data: { ...baseData, - id_ats_department: uuidv4(), + id_ecom_fulfillment: uuidv4(), created_at: new Date(), remote_id: originId, id_connection: connection_id, @@ -172,30 +170,30 @@ export class SyncService implements OnModuleInit, IBaseSync { } }; - for (let i = 0; i < departments.length; i++) { - const department = departments[i]; - const originId = department.remote_id; + for (let i = 0; i < fulfillments.length; i++) { + const fulfillment = fulfillments[i]; + const originId = fulfillment.remote_id; - const res = await updateOrCreateDepartment(department, originId); - const department_id = res.id_ats_department; - departments_results.push(res); + const res = await updateOrCreateFulfillment(fulfillment, originId); + const fulfillment_id = res.id_ecom_fulfilment; + fulfillments_results.push(res); // Process field mappings await this.ingestService.processFieldMappings( - department.field_mappings, - department_id, + fulfillment.field_mappings, + fulfillment_id, originSource, linkedUserId, ); // Process remote data await this.ingestService.processRemoteData( - department_id, + fulfillment_id, remote_data[i], ); } - return departments_results; + return fulfillments_results; } catch (error) { throw error; } diff --git a/packages/api/src/ecommerce/fulfillment/types/index.ts b/packages/api/src/ecommerce/fulfillment/types/index.ts index 2cca2d7d6..6aa2a0e08 100644 --- a/packages/api/src/ecommerce/fulfillment/types/index.ts +++ b/packages/api/src/ecommerce/fulfillment/types/index.ts @@ -1,24 +1,19 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; import { - UnifiedDepartmentInput, - UnifiedDepartmentOutput, + UnifiedFulfillmentInput, + UnifiedFulfillmentOutput, } from './model.unified'; -import { OriginalDepartmentOutput } from '@@core/utils/types/original/original.ats'; +import { OriginalFulfillmentOutput } from '@@core/utils/types/original/original.ecommerce'; import { ApiResponse } from '@@core/utils/types'; import { IBaseObjectService, SyncParam } from '@@core/utils/types/interface'; -export interface IDepartmentService extends IBaseObjectService { - addDepartment( - departmentData: DesunifyReturnType, - linkedUserId: string, - ): Promise>; - - sync(data: SyncParam): Promise>; +export interface IFulfillmentService extends IBaseObjectService { + sync(data: SyncParam): Promise>; } -export interface IDepartmentMapper { +export interface IFulfillmentMapper { desunify( - source: UnifiedDepartmentInput, + source: UnifiedFulfillmentInput, customFieldMappings?: { slug: string; remote_id: string; @@ -26,11 +21,11 @@ export interface IDepartmentMapper { ): DesunifyReturnType; unify( - source: OriginalDepartmentOutput | OriginalDepartmentOutput[], + source: OriginalFulfillmentOutput | OriginalFulfillmentOutput[], connectionId: string, customFieldMappings?: { slug: string; remote_id: string; }[], - ): Promise; + ): Promise; } diff --git a/packages/api/src/ecommerce/fulfillment/types/model.unified.ts b/packages/api/src/ecommerce/fulfillment/types/model.unified.ts index a874dcf6b..0778ebca6 100644 --- a/packages/api/src/ecommerce/fulfillment/types/model.unified.ts +++ b/packages/api/src/ecommerce/fulfillment/types/model.unified.ts @@ -1,61 +1,88 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsUUID, IsOptional, IsString, IsDateString } from 'class-validator'; +import { + IsUUID, + IsOptional, + IsString, + IsDateString, + IsArray, + IsObject, +} from 'class-validator'; -export class UnifiedDepartmentInput { +export class UnifiedFulfilmentInput { @ApiPropertyOptional({ type: String, - description: 'The name of the department', + description: 'The carrier of the fulfilment', }) @IsString() @IsOptional() - name?: string; + carrier?: string; @ApiPropertyOptional({ - type: {}, - description: - 'The custom field mappings of the object between the remote 3rd party & Panora', + type: [String], + description: 'The tracking URLs of the fulfilment', }) + @IsArray() @IsOptional() - field_mappings?: Record; -} + @IsString({ each: true }) + tracking_urls?: string[]; + + @ApiPropertyOptional({ + type: [String], + description: 'The tracking numbers of the fulfilment', + }) + @IsArray() + @IsOptional() + @IsString({ each: true }) + tracking_numbers?: string[]; + + @ApiPropertyOptional({ + type: Object, + description: 'The items in the fulfilment', + }) + @IsObject() + @IsOptional() + items?: Record; -export class UnifiedDepartmentOutput extends UnifiedDepartmentInput { @ApiPropertyOptional({ type: String, - description: 'The UUID of the department', + description: 'The UUID of the order associated with the fulfilment', }) @IsUUID() @IsOptional() - id?: string; + order_id?: string; +} +export class UnifiedFulfilmentOutput extends UnifiedFulfilmentInput { @ApiPropertyOptional({ type: String, - description: - 'The remote ID of the department in the context of the 3rd Party', + description: 'The UUID of the fulfilment', }) - @IsString() + @IsUUID() @IsOptional() - remote_id?: string; + id: string; @ApiPropertyOptional({ - type: {}, + type: String, description: - 'The remote data of the department in the context of the 3rd Party', + 'The remote ID of the fulfilment in the context of the 3rd Party', }) + @IsString() @IsOptional() - remote_data?: Record; + remote_id?: string; @ApiPropertyOptional({ - type: {}, + type: String, description: 'The created date of the object', }) + @IsDateString() @IsOptional() - created_at?: any; + created_at?: string; @ApiPropertyOptional({ - type: {}, + type: String, description: 'The modified date of the object', }) + @IsDateString() @IsOptional() - modified_at?: any; + modified_at?: string; } diff --git a/packages/api/src/ecommerce/fulfillmentorders/fulfillmentorders.controller.ts b/packages/api/src/ecommerce/fulfillmentorders/fulfillmentorders.controller.ts index 18f984923..256968753 100644 --- a/packages/api/src/ecommerce/fulfillmentorders/fulfillmentorders.controller.ts +++ b/packages/api/src/ecommerce/fulfillmentorders/fulfillmentorders.controller.ts @@ -1,47 +1,40 @@ +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +import { ApiCustomResponse } from '@@core/utils/types'; import { Controller, - Post, - Body, - Query, Get, - Patch, - Param, Headers, + Param, + Query, UseGuards, } from '@nestjs/common'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { - ApiBody, + ApiHeader, ApiOperation, ApiParam, ApiQuery, ApiTags, - ApiHeader, } from '@nestjs/swagger'; -import { ApiCustomResponse } from '@@core/utils/types'; -import { DepartmentService } from './services/department.service'; -import { - UnifiedDepartmentInput, - UnifiedDepartmentOutput, -} from './types/model.unified'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; -import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +import { FulfillmentOrdersService } from './services/fulfillmentorders.service'; +import { UnifiedFulfillmentOrdersOutput } from './types/model.unified'; -@ApiTags('ats/department') -@Controller('ats/department') -export class DepartmentController { +@ApiTags('ats/fulfillmentorders') +@Controller('ats/fulfillmentorders') +export class FulfillmentOrdersController { constructor( - private readonly departmentService: DepartmentService, + private readonly fulfillmentordersService: FulfillmentOrdersService, private logger: LoggerService, private connectionUtils: ConnectionUtils, ) { - this.logger.setContext(DepartmentController.name); + this.logger.setContext(FulfillmentOrdersController.name); } @ApiOperation({ - operationId: 'getDepartments', - summary: 'List a batch of Departments', + operationId: 'getFulfillmentOrderss', + summary: 'List a batch of FulfillmentOrderss', }) @ApiHeader({ name: 'x-connection-token', @@ -49,10 +42,10 @@ export class DepartmentController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiCustomResponse(UnifiedDepartmentOutput) + @ApiCustomResponse(UnifiedFulfillmentOrdersOutput) @UseGuards(ApiKeyAuthGuard) @Get() - async getDepartments( + async getFulfillmentOrderss( @Headers('x-connection-token') connection_token: string, @Query() query: FetchObjectsQueryDto, ) { @@ -62,7 +55,7 @@ export class DepartmentController { connection_token, ); const { remote_data, limit, cursor } = query; - return this.departmentService.getDepartments( + return this.fulfillmentordersService.getFulfillmentOrderss( connectionId, remoteSource, linkedUserId, @@ -76,21 +69,23 @@ export class DepartmentController { } @ApiOperation({ - operationId: 'getDepartment', - summary: 'Retrieve a Department', - description: 'Retrieve a department from any connected Ats software', + operationId: 'getFulfillmentOrders', + summary: 'Retrieve a FulfillmentOrders', + description: + 'Retrieve a fulfillmentorders from any connected Ecommerce software', }) @ApiParam({ name: 'id', required: true, type: String, - description: 'id of the department you want to retrieve.', + description: 'id of the fulfillmentorders you want to retrieve.', }) @ApiQuery({ name: 'remote_data', required: false, type: Boolean, - description: 'Set to true to include data from the original Ats software.', + description: + 'Set to true to include data from the original Ecommerce software.', }) @ApiHeader({ name: 'x-connection-token', @@ -98,7 +93,7 @@ export class DepartmentController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiCustomResponse(UnifiedDepartmentOutput) + @ApiCustomResponse(UnifiedFulfillmentOrdersOutput) @UseGuards(ApiKeyAuthGuard) @Get(':id') async retrieve( @@ -110,7 +105,7 @@ export class DepartmentController { await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); - return this.departmentService.getDepartment( + return this.fulfillmentordersService.getFulfillmentOrders( id, linkedUserId, remoteSource, diff --git a/packages/api/src/ecommerce/fulfillmentorders/fulfillmentorders.module.ts b/packages/api/src/ecommerce/fulfillmentorders/fulfillmentorders.module.ts index 2b2128304..9577253cb 100644 --- a/packages/api/src/ecommerce/fulfillmentorders/fulfillmentorders.module.ts +++ b/packages/api/src/ecommerce/fulfillmentorders/fulfillmentorders.module.ts @@ -1,41 +1,31 @@ -import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; +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 { ConnectionUtils } from '@@core/connections/@utils'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Utils } from '@ecommerce/@lib/@utils'; import { Module } from '@nestjs/common'; -import { DepartmentController } from './fulfillmentorders.controller'; -import { DepartmentService } from './services/department.service'; +import { FulfillmentOrdersController } from './fulfillmentorders.controller'; +import { FulfillmentOrdersService } from './services/fulfillmentorders.service'; import { ServiceRegistry } from './services/registry.service'; +import { ShopifyService } from './services/shopify'; +import { ShopifyFulfillmentOrdersMapper } from './services/shopify/mappers'; import { SyncService } from './sync/sync.service'; -import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { AshbyService } from './services/ashby'; - -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; - -import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; -import { AshbyDepartmentMapper } from './services/ashby/mappers'; -import { Utils } from '@ats/@lib/@utils'; @Module({ imports: [BullQueueModule], - controllers: [DepartmentController], + controllers: [FulfillmentOrdersController], providers: [ - DepartmentService, + FulfillmentOrdersService, CoreUnification, - SyncService, WebhookService, - ServiceRegistry, - IngestDataService, - Utils, - AshbyDepartmentMapper, + ShopifyFulfillmentOrdersMapper, /* PROVIDERS SERVICES */ - AshbyService, + ShopifyService, ], exports: [SyncService], }) -export class DepartmentModule {} +export class FulfillmentOrdersModule {} diff --git a/packages/api/src/ecommerce/fulfillmentorders/services/ashby/mappers.ts b/packages/api/src/ecommerce/fulfillmentorders/services/ashby/mappers.ts deleted file mode 100644 index 877efdf88..000000000 --- a/packages/api/src/ecommerce/fulfillmentorders/services/ashby/mappers.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { AshbyDepartmentInput, AshbyDepartmentOutput } from './types'; -import { - UnifiedDepartmentInput, - UnifiedDepartmentOutput, -} from '@ats/department/types/model.unified'; -import { IDepartmentMapper } from '@ats/department/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 '@ats/@lib/@utils'; - -@Injectable() -export class AshbyDepartmentMapper implements IDepartmentMapper { - constructor( - private mappersRegistry: MappersRegistry, - private utils: Utils, - private coreUnificationService: CoreUnification, - ) { - this.mappersRegistry.registerService('ats', 'department', 'ashby', this); - } - - async desunify( - source: UnifiedDepartmentInput, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): Promise { - return; - } - - async unify( - source: AshbyDepartmentOutput | AshbyDepartmentOutput[], - connectionId: string, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): Promise { - if (!Array.isArray(source)) { - return await this.mapSingleDepartmentToUnified( - source, - connectionId, - customFieldMappings, - ); - } - // Handling array of AshbyDepartmentOutput - return Promise.all( - source.map((department) => - this.mapSingleDepartmentToUnified( - department, - connectionId, - customFieldMappings, - ), - ), - ); - } - - private async mapSingleDepartmentToUnified( - department: AshbyDepartmentOutput, - connectionId: string, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): Promise { - return { - remote_id: department.id, - remote_data: department, - name: department.name || null, - }; - } -} diff --git a/packages/api/src/ecommerce/fulfillmentorders/services/ashby/types.ts b/packages/api/src/ecommerce/fulfillmentorders/services/ashby/types.ts deleted file mode 100644 index d3c47509a..000000000 --- a/packages/api/src/ecommerce/fulfillmentorders/services/ashby/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface AshbyDepartmentInput { - id: string; - name: string; - isArchived: boolean; - parentId: string; -} - -export type AshbyDepartmentOutput = Partial; diff --git a/packages/api/src/ecommerce/fulfillmentorders/services/fulfillmentorders.service.ts b/packages/api/src/ecommerce/fulfillmentorders/services/fulfillmentorders.service.ts new file mode 100644 index 000000000..c27a1e2fe --- /dev/null +++ b/packages/api/src/ecommerce/fulfillmentorders/services/fulfillmentorders.service.ts @@ -0,0 +1,245 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { v4 as uuidv4 } from 'uuid'; +import { UnifiedFulfillmentOrdersOutput } from '../types/model.unified'; + +@Injectable() +export class FulfillmentOrdersService { + constructor(private prisma: PrismaService, private logger: LoggerService) { + this.logger.setContext(FulfillmentOrdersService.name); + } + + async getFulfillmentOrders( + id_ecommerce_fulfillmentorders: string, + linkedUserId: string, + integrationId: string, + remote_data?: boolean, + ): Promise { + try { + const fulfillmentorders = + await this.prisma.ecommerce_fulfillmentorderss.findUnique({ + where: { + id_ecommerce_fulfillmentorders: id_ecommerce_fulfillmentorders, + }, + }); + + if (!fulfillmentorders) { + throw new Error( + `FulfillmentOrders with ID ${id_ecommerce_fulfillmentorders} not found.`, + ); + } + + // Fetch field mappings for the fulfillmentorders + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: + fulfillmentorders.id_ecommerce_fulfillmentorders, + }, + }, + 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 = Array.from(fieldMappingsMap, ([key, value]) => ({ + [key]: value, + })); + + // Transform to UnifiedFulfillmentOrdersOutput format + const unifiedFulfillmentOrders: UnifiedFulfillmentOrdersOutput = { + id: fulfillmentorders.id_ecommerce_fulfillmentorders, + name: fulfillmentorders.name, + field_mappings: field_mappings, + remote_id: fulfillmentorders.remote_id, + created_at: fulfillmentorders.created_at, + modified_at: fulfillmentorders.modified_at, + }; + + let res: UnifiedFulfillmentOrdersOutput = unifiedFulfillmentOrders; + if (remote_data) { + const resp = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: + fulfillmentorders.id_ecommerce_fulfillmentorders, + }, + }); + const remote_data = JSON.parse(resp.data); + + res = { + ...res, + remote_data: remote_data, + }; + } + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'ecommerce.fulfillmentorders.pull', + method: 'GET', + url: '/ecommerce/fulfillmentorders', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + + return res; + } catch (error) { + throw error; + } + } + + async getFulfillmentOrderss( + connection_id: string, + integrationId: string, + linkedUserId: string, + limit: number, + remote_data?: boolean, + cursor?: string, + ): Promise<{ + data: UnifiedFulfillmentOrdersOutput[]; + prev_cursor: null | string; + next_cursor: null | string; + }> { + try { + let prev_cursor = null; + let next_cursor = null; + + if (cursor) { + const isCursorPresent = + await this.prisma.ecommerce_fulfillmentorderss.findFirst({ + where: { + id_connection: connection_id, + id_ecommerce_fulfillmentorders: cursor, + }, + }); + if (!isCursorPresent) { + throw new ReferenceError(`The provided cursor does not exist!`); + } + } + + const fulfillmentorderss = + await this.prisma.ecommerce_fulfillmentorderss.findMany({ + take: limit + 1, + cursor: cursor + ? { + id_ecommerce_fulfillmentorders: cursor, + } + : undefined, + orderBy: { + created_at: 'asc', + }, + where: { + id_connection: connection_id, + }, + }); + + if (fulfillmentorderss.length === limit + 1) { + next_cursor = Buffer.from( + fulfillmentorderss[fulfillmentorderss.length - 1] + .id_ecommerce_fulfillmentorders, + ).toString('base64'); + fulfillmentorderss.pop(); + } + + if (cursor) { + prev_cursor = Buffer.from(cursor).toString('base64'); + } + + const unifiedFulfillmentOrderss: UnifiedFulfillmentOrdersOutput[] = + await Promise.all( + fulfillmentorderss.map(async (fulfillmentorders) => { + // Fetch field mappings for the fulfillmentorders + const values = await this.prisma.value.findMany({ + where: { + entity: { + ressource_owner_id: + fulfillmentorders.id_ecommerce_fulfillmentorders, + }, + }, + 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 = Array.from( + fieldMappingsMap, + ([key, value]) => ({ + [key]: value, + }), + ); + + // Transform to UnifiedFulfillmentOrdersOutput format + return { + id: fulfillmentorders.id_ecommerce_fulfillmentorders, + name: fulfillmentorders.name, + field_mappings: field_mappings, + remote_id: fulfillmentorders.remote_id, + created_at: fulfillmentorders.created_at, + modified_at: fulfillmentorders.modified_at, + }; + }), + ); + + let res: UnifiedFulfillmentOrdersOutput[] = unifiedFulfillmentOrderss; + + if (remote_data) { + const remote_array_data: UnifiedFulfillmentOrdersOutput[] = + await Promise.all( + res.map(async (fulfillmentorders) => { + const resp = await this.prisma.remote_data.findFirst({ + where: { + ressource_owner_id: fulfillmentorders.id, + }, + }); + const remote_data = JSON.parse(resp.data); + return { ...fulfillmentorders, remote_data }; + }), + ); + + res = remote_array_data; + } + + await this.prisma.events.create({ + data: { + id_event: uuidv4(), + status: 'success', + type: 'ecommerce.fulfillmentorders.pull', + method: 'GET', + url: '/ecommerce/fulfillmentorderss', + provider: integrationId, + direction: '0', + timestamp: new Date(), + id_linked_user: linkedUserId, + }, + }); + + return { + data: res, + prev_cursor, + next_cursor, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/ecommerce/fulfillmentorders/services/registry.service.ts b/packages/api/src/ecommerce/fulfillmentorders/services/registry.service.ts index ee946e7b7..31d4d35b6 100644 --- a/packages/api/src/ecommerce/fulfillmentorders/services/registry.service.ts +++ b/packages/api/src/ecommerce/fulfillmentorders/services/registry.service.ts @@ -1,19 +1,19 @@ import { Injectable } from '@nestjs/common'; -import { IDepartmentService } from '../types'; +import { IFulfillmentOrdersService } from '../types'; @Injectable() export class ServiceRegistry { - private serviceMap: Map; + private serviceMap: Map; constructor() { - this.serviceMap = new Map(); + this.serviceMap = new Map(); } - registerService(serviceKey: string, service: IDepartmentService) { + registerService(serviceKey: string, service: IFulfillmentOrdersService) { this.serviceMap.set(serviceKey, service); } - getService(integrationId: string): IDepartmentService { + getService(integrationId: string): IFulfillmentOrdersService { const service = this.serviceMap.get(integrationId); if (!service) { throw new ReferenceError( diff --git a/packages/api/src/ecommerce/fulfillmentorders/services/ashby/index.ts b/packages/api/src/ecommerce/fulfillmentorders/services/shopify/index.ts similarity index 57% rename from packages/api/src/ecommerce/fulfillmentorders/services/ashby/index.ts rename to packages/api/src/ecommerce/fulfillmentorders/services/shopify/index.ts index cceda214e..e736358cc 100644 --- a/packages/api/src/ecommerce/fulfillmentorders/services/ashby/index.ts +++ b/packages/api/src/ecommerce/fulfillmentorders/services/shopify/index.ts @@ -1,20 +1,19 @@ -import { Injectable } from '@nestjs/common'; -import { IDepartmentService } from '@ats/department/types'; -import { AtsObject } from '@ats/@lib/@types'; -import axios from 'axios'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { ActionType, handle3rdPartyServiceError } from '@@core/utils/errors'; 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 { ServiceRegistry } from '../registry.service'; -import { AshbyDepartmentInput, AshbyDepartmentOutput } from './types'; import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { OriginalDepartmentOutput } from '@@core/utils/types/original/original.ats'; import { SyncParam } from '@@core/utils/types/interface'; +import { OriginalFulfillmentOrdersOutput } from '@@core/utils/types/original/original.ecommerce'; +import { IFulfillmentOrdersService } from '@ecommerce/fulfillmentorders/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { ShopifyFulfillmentOrdersOutput } from './types'; +import { EcommerceObject } from '@panora/shared'; @Injectable() -export class AshbyService implements IDepartmentService { +export class ShopifyService implements IFulfillmentOrdersService { constructor( private prisma: PrismaService, private logger: LoggerService, @@ -22,26 +21,24 @@ export class AshbyService implements IDepartmentService { private registry: ServiceRegistry, ) { this.logger.setContext( - AtsObject.department.toUpperCase() + ':' + AshbyService.name, + EcommerceObject.fulfillmentorders.toUpperCase() + + ':' + + ShopifyService.name, ); - this.registry.registerService('ashby', this); - } - addDepartment( - departmentData: DesunifyReturnType, - linkedUserId: string, - ): Promise> { - throw new Error('Method not implemented.'); + this.registry.registerService('shopify', this); } - async sync(data: SyncParam): Promise> { + async sync( + data: SyncParam, + ): Promise> { try { const { linkedUserId } = data; const connection = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, - provider_slug: 'ashby', - vertical: 'ats', + provider_slug: 'shopify', + vertical: 'ecommerce', }, }); const resp = await axios.post( @@ -55,12 +52,13 @@ export class AshbyService implements IDepartmentService { }, }, ); - const departments: AshbyDepartmentOutput[] = resp.data.results; - this.logger.log(`Synced ashby departments !`); + const fulfillmentorderss: ShopifyFulfillmentOrdersOutput[] = + resp.data.results; + this.logger.log(`Synced shopify fulfillmentorderss !`); return { - data: departments, - message: 'Ashby departments retrieved', + data: fulfillmentorderss, + message: 'Shopify fulfillmentorderss retrieved', statusCode: 200, }; } catch (error) { diff --git a/packages/api/src/ecommerce/fulfillmentorders/services/shopify/mappers.ts b/packages/api/src/ecommerce/fulfillmentorders/services/shopify/mappers.ts new file mode 100644 index 000000000..582c03041 --- /dev/null +++ b/packages/api/src/ecommerce/fulfillmentorders/services/shopify/mappers.ts @@ -0,0 +1,85 @@ +import { + ShopifyFulfillmentOrdersInput, + ShopifyFulfillmentOrdersOutput, +} from './types'; +import { + UnifiedFulfillmentOrdersInput, + UnifiedFulfillmentOrdersOutput, +} from '@ecommerce/fulfillmentorders/types/model.unified'; +import { IFulfillmentOrdersMapper } from '@ecommerce/fulfillmentorders/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 ShopifyFulfillmentOrdersMapper + implements IFulfillmentOrdersMapper +{ + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService( + 'ecommerce', + 'fulfillmentorders', + 'shopify', + this, + ); + } + + async desunify( + source: UnifiedFulfillmentOrdersInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + return; + } + + async unify( + source: ShopifyFulfillmentOrdersOutput | ShopifyFulfillmentOrdersOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise< + UnifiedFulfillmentOrdersOutput | UnifiedFulfillmentOrdersOutput[] + > { + if (!Array.isArray(source)) { + return await this.mapSingleFulfillmentOrdersToUnified( + source, + connectionId, + customFieldMappings, + ); + } + // Handling array of ShopifyFulfillmentOrdersOutput + return Promise.all( + source.map((fulfillmentorders) => + this.mapSingleFulfillmentOrdersToUnified( + fulfillmentorders, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleFulfillmentOrdersToUnified( + fulfillmentorders: ShopifyFulfillmentOrdersOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + return { + remote_id: fulfillmentorders.id, + remote_data: fulfillmentorders, + name: fulfillmentorders.name || null, + }; + } +} diff --git a/packages/api/src/ecommerce/fulfillmentorders/services/shopify/types.ts b/packages/api/src/ecommerce/fulfillmentorders/services/shopify/types.ts new file mode 100644 index 000000000..dd46f2db2 --- /dev/null +++ b/packages/api/src/ecommerce/fulfillmentorders/services/shopify/types.ts @@ -0,0 +1,6 @@ +export interface ShopifyFulfillmentOrdersInput { + [key: string]: any; +} + +export type ShopifyFulfillmentOrdersOutput = + Partial; diff --git a/packages/api/src/ecommerce/fulfillmentorders/sync/sync.processor.ts b/packages/api/src/ecommerce/fulfillmentorders/sync/sync.processor.ts index 4c01a02f0..1b454bbb0 100644 --- a/packages/api/src/ecommerce/fulfillmentorders/sync/sync.processor.ts +++ b/packages/api/src/ecommerce/fulfillmentorders/sync/sync.processor.ts @@ -7,13 +7,15 @@ import { Queues } from '@@core/@core-services/queues/types'; export class SyncProcessor { constructor(private syncService: SyncService) {} - @Process('ats-sync-departments') - async handleSyncDepartments(job: Job) { + @Process('ecommerce-sync-fulfillmentorderss') + async handleSyncFulfillmentOrderss(job: Job) { try { - console.log(`Processing queue -> ats-sync-departments ${job.id}`); + console.log( + `Processing queue -> ecommerce-sync-fulfillmentorderss ${job.id}`, + ); await this.syncService.kickstartSync(); } catch (error) { - console.error('Error syncing ats departments', error); + console.error('Error syncing ecommerce fulfillmentorderss', error); } } } diff --git a/packages/api/src/ecommerce/fulfillmentorders/sync/sync.service.ts b/packages/api/src/ecommerce/fulfillmentorders/sync/sync.service.ts index 61c7cbe95..ee58da5b4 100644 --- a/packages/api/src/ecommerce/fulfillmentorders/sync/sync.service.ts +++ b/packages/api/src/ecommerce/fulfillmentorders/sync/sync.service.ts @@ -1,23 +1,21 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { BullQueueService } from '@@core/@core-services/queues/shared.service'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { OriginalFulfillmentOrdersOutput } from '@@core/utils/types/original/original.ecommerce'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; +import { ATS_PROVIDERS, ECOMMERCE_PROVIDERS } from '@panora/shared'; +import { ecommerce_fulfillmentorders as EcommerceFulfillmentOrders } from '@prisma/client'; import { v4 as uuidv4 } from 'uuid'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; -import { ApiResponse } from '@@core/utils/types'; -import { IDepartmentService } from '../types'; -import { OriginalDepartmentOutput } from '@@core/utils/types/original/original.ats'; -import { UnifiedDepartmentOutput } from '../types/model.unified'; -import { ats_departments as AtsDepartment } from '@prisma/client'; -import { ATS_PROVIDERS } from '@panora/shared'; -import { AtsObject } from '@ats/@lib/@types'; -import { BullQueueService } from '@@core/@core-services/queues/shared.service'; -import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; -import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IFulfillmentOrdersService } from '../types'; +import { UnifiedFulfillmentOrdersOutput } from '../types/model.unified'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -33,13 +31,13 @@ export class SyncService implements OnModuleInit, IBaseSync { private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); - this.registry.registerService('ats', 'department', this); + this.registry.registerService('ecommerce', 'fulfillmentorders', this); } async onModuleInit() { try { await this.bullQueueService.queueSyncJob( - 'ats-sync-departments', + 'ecommerce-sync-fulfillmentorderss', '0 0 * * *', ); } catch (error) { @@ -50,7 +48,7 @@ export class SyncService implements OnModuleInit, IBaseSync { @Cron('0 */8 * * *') // every 8 hours async kickstartSync(user_id?: string) { try { - this.logger.log('Syncing departments...'); + this.logger.log('Syncing fulfillmentorderss...'); const users = user_id ? [ await this.prisma.users.findUnique({ @@ -76,7 +74,7 @@ export class SyncService implements OnModuleInit, IBaseSync { }); linkedUsers.map(async (linkedUser) => { try { - const providers = ATS_PROVIDERS; + const providers = ECOMMERCE_PROVIDERS; for (const provider of providers) { try { await this.syncForLinkedUser({ @@ -102,15 +100,22 @@ export class SyncService implements OnModuleInit, IBaseSync { async syncForLinkedUser(param: SyncLinkedUserType) { try { const { integrationId, linkedUserId } = param; - const service: IDepartmentService = + const service: IFulfillmentOrdersService = this.serviceRegistry.getService(integrationId); if (!service) return; await this.ingestService.syncForLinkedUser< - UnifiedDepartmentOutput, - OriginalDepartmentOutput, - IDepartmentService - >(integrationId, linkedUserId, 'ats', 'department', service, []); + UnifiedFulfillmentOrdersOutput, + OriginalFulfillmentOrdersOutput, + IFulfillmentOrdersService + >( + integrationId, + linkedUserId, + 'ecommerce', + 'fulfillmentorders', + service, + [], + ); } catch (error) { throw error; } @@ -119,51 +124,54 @@ export class SyncService implements OnModuleInit, IBaseSync { async saveToDb( connection_id: string, linkedUserId: string, - departments: UnifiedDepartmentOutput[], + fulfillmentorderss: UnifiedFulfillmentOrdersOutput[], originSource: string, remote_data: Record[], - ): Promise { + ): Promise { try { - const departments_results: AtsDepartment[] = []; + const fulfillmentorderss_results: EcommerceFulfillmentOrders[] = []; - const updateOrCreateDepartment = async ( - department: UnifiedDepartmentOutput, + const updateOrCreateFulfillmentOrders = async ( + fulfillmentorders: UnifiedFulfillmentOrdersOutput, originId: string, ) => { - let existingDepartment; + let existingFulfillmentOrders; if (!originId) { - existingDepartment = await this.prisma.ats_departments.findFirst({ - where: { - name: department.name, - id_connection: connection_id, - }, - }); + existingFulfillmentOrders = + await this.prisma.ecommerce_fulfillmentorderss.findFirst({ + where: { + name: fulfillmentorders.name, + id_connection: connection_id, + }, + }); } else { - existingDepartment = await this.prisma.ats_departments.findFirst({ - where: { - remote_id: originId, - id_connection: connection_id, - }, - }); + existingFulfillmentOrders = + await this.prisma.ecommerce_fulfillmentorderss.findFirst({ + where: { + remote_id: originId, + id_connection: connection_id, + }, + }); } const baseData: any = { - name: department.name ?? null, + name: fulfillmentorders.name ?? null, modified_at: new Date(), }; - if (existingDepartment) { - return await this.prisma.ats_departments.update({ + if (existingFulfillmentOrders) { + return await this.prisma.ecommerce_fulfillmentorderss.update({ where: { - id_ats_department: existingDepartment.id_ats_department, + id_ecommerce_fulfillmentorders: + existingFulfillmentOrders.id_ecommerce_fulfillmentorders, }, data: baseData, }); } else { - return await this.prisma.ats_departments.create({ + return await this.prisma.ecommerce_fulfillmentorderss.create({ data: { ...baseData, - id_ats_department: uuidv4(), + id_ecommerce_fulfillmentorders: uuidv4(), created_at: new Date(), remote_id: originId, id_connection: connection_id, @@ -172,30 +180,33 @@ export class SyncService implements OnModuleInit, IBaseSync { } }; - for (let i = 0; i < departments.length; i++) { - const department = departments[i]; - const originId = department.remote_id; + for (let i = 0; i < fulfillmentorderss.length; i++) { + const fulfillmentorders = fulfillmentorderss[i]; + const originId = fulfillmentorders.remote_id; - const res = await updateOrCreateDepartment(department, originId); - const department_id = res.id_ats_department; - departments_results.push(res); + const res = await updateOrCreateFulfillmentOrders( + fulfillmentorders, + originId, + ); + const fulfillmentorders_id = res.id_ecommerce_fulfillmentorders; + fulfillmentorderss_results.push(res); // Process field mappings await this.ingestService.processFieldMappings( - department.field_mappings, - department_id, + fulfillmentorders.field_mappings, + fulfillmentorders_id, originSource, linkedUserId, ); // Process remote data await this.ingestService.processRemoteData( - department_id, + fulfillmentorders_id, remote_data[i], ); } - return departments_results; + return fulfillmentorderss_results; } catch (error) { throw error; } diff --git a/packages/api/src/ecommerce/fulfillmentorders/types/index.ts b/packages/api/src/ecommerce/fulfillmentorders/types/index.ts index 2cca2d7d6..2a31c8503 100644 --- a/packages/api/src/ecommerce/fulfillmentorders/types/index.ts +++ b/packages/api/src/ecommerce/fulfillmentorders/types/index.ts @@ -1,24 +1,21 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; import { - UnifiedDepartmentInput, - UnifiedDepartmentOutput, + UnifiedFulfillmentOrdersInput, + UnifiedFulfillmentOrdersOutput, } from './model.unified'; -import { OriginalDepartmentOutput } from '@@core/utils/types/original/original.ats'; +import { OriginalFulfillmentOrdersOutput } from '@@core/utils/types/original/original.ecommerce'; import { ApiResponse } from '@@core/utils/types'; import { IBaseObjectService, SyncParam } from '@@core/utils/types/interface'; -export interface IDepartmentService extends IBaseObjectService { - addDepartment( - departmentData: DesunifyReturnType, - linkedUserId: string, - ): Promise>; - - sync(data: SyncParam): Promise>; +export interface IFulfillmentOrdersService extends IBaseObjectService { + sync( + data: SyncParam, + ): Promise>; } -export interface IDepartmentMapper { +export interface IFulfillmentOrdersMapper { desunify( - source: UnifiedDepartmentInput, + source: UnifiedFulfillmentOrdersInput, customFieldMappings?: { slug: string; remote_id: string; @@ -26,11 +23,11 @@ export interface IDepartmentMapper { ): DesunifyReturnType; unify( - source: OriginalDepartmentOutput | OriginalDepartmentOutput[], + source: OriginalFulfillmentOrdersOutput | OriginalFulfillmentOrdersOutput[], connectionId: string, customFieldMappings?: { slug: string; remote_id: string; }[], - ): Promise; + ): Promise; } diff --git a/packages/api/src/ecommerce/fulfillmentorders/types/model.unified.ts b/packages/api/src/ecommerce/fulfillmentorders/types/model.unified.ts index a874dcf6b..816fe0293 100644 --- a/packages/api/src/ecommerce/fulfillmentorders/types/model.unified.ts +++ b/packages/api/src/ecommerce/fulfillmentorders/types/model.unified.ts @@ -1,10 +1,10 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsUUID, IsOptional, IsString, IsDateString } from 'class-validator'; -export class UnifiedDepartmentInput { +export class UnifiedFulfillmentOrdersInput { @ApiPropertyOptional({ type: String, - description: 'The name of the department', + description: 'The name of the fulfillmentorders', }) @IsString() @IsOptional() @@ -19,10 +19,10 @@ export class UnifiedDepartmentInput { field_mappings?: Record; } -export class UnifiedDepartmentOutput extends UnifiedDepartmentInput { +export class UnifiedFulfillmentOrdersOutput extends UnifiedFulfillmentOrdersInput { @ApiPropertyOptional({ type: String, - description: 'The UUID of the department', + description: 'The UUID of the fulfillmentorders', }) @IsUUID() @IsOptional() @@ -31,7 +31,7 @@ export class UnifiedDepartmentOutput extends UnifiedDepartmentInput { @ApiPropertyOptional({ type: String, description: - 'The remote ID of the department in the context of the 3rd Party', + 'The remote ID of the fulfillmentorders in the context of the 3rd Party', }) @IsString() @IsOptional() @@ -40,7 +40,7 @@ export class UnifiedDepartmentOutput extends UnifiedDepartmentInput { @ApiPropertyOptional({ type: {}, description: - 'The remote data of the department in the context of the 3rd Party', + 'The remote data of the fulfillmentorders in the context of the 3rd Party', }) @IsOptional() remote_data?: Record; diff --git a/packages/api/src/ecommerce/order/order.controller.ts b/packages/api/src/ecommerce/order/order.controller.ts index 18f984923..88282a954 100644 --- a/packages/api/src/ecommerce/order/order.controller.ts +++ b/packages/api/src/ecommerce/order/order.controller.ts @@ -1,47 +1,40 @@ +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +import { ApiCustomResponse } from '@@core/utils/types'; import { Controller, - Post, - Body, - Query, Get, - Patch, - Param, Headers, + Param, + Query, UseGuards, } from '@nestjs/common'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { - ApiBody, + ApiHeader, ApiOperation, ApiParam, ApiQuery, ApiTags, - ApiHeader, } from '@nestjs/swagger'; -import { ApiCustomResponse } from '@@core/utils/types'; -import { DepartmentService } from './services/department.service'; -import { - UnifiedDepartmentInput, - UnifiedDepartmentOutput, -} from './types/model.unified'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; -import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +import { OrderService } from './services/order.service'; +import { UnifiedOrderOutput } from './types/model.unified'; -@ApiTags('ats/department') -@Controller('ats/department') -export class DepartmentController { +@ApiTags('ecommerce/order') +@Controller('ecommerce/order') +export class OrderController { constructor( - private readonly departmentService: DepartmentService, + private readonly orderService: OrderService, private logger: LoggerService, private connectionUtils: ConnectionUtils, ) { - this.logger.setContext(DepartmentController.name); + this.logger.setContext(OrderController.name); } @ApiOperation({ - operationId: 'getDepartments', - summary: 'List a batch of Departments', + operationId: 'getOrders', + summary: 'List a batch of Orders', }) @ApiHeader({ name: 'x-connection-token', @@ -49,10 +42,10 @@ export class DepartmentController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiCustomResponse(UnifiedDepartmentOutput) + @ApiCustomResponse(UnifiedOrderOutput) @UseGuards(ApiKeyAuthGuard) @Get() - async getDepartments( + async getOrders( @Headers('x-connection-token') connection_token: string, @Query() query: FetchObjectsQueryDto, ) { @@ -62,7 +55,7 @@ export class DepartmentController { connection_token, ); const { remote_data, limit, cursor } = query; - return this.departmentService.getDepartments( + return this.orderService.getOrders( connectionId, remoteSource, linkedUserId, @@ -76,15 +69,15 @@ export class DepartmentController { } @ApiOperation({ - operationId: 'getDepartment', - summary: 'Retrieve a Department', - description: 'Retrieve a department from any connected Ats software', + operationId: 'getOrder', + summary: 'Retrieve a Order', + description: 'Retrieve a order from any connected Ats software', }) @ApiParam({ name: 'id', required: true, type: String, - description: 'id of the department you want to retrieve.', + description: 'id of the order you want to retrieve.', }) @ApiQuery({ name: 'remote_data', @@ -98,7 +91,7 @@ export class DepartmentController { description: 'The connection token', example: 'b008e199-eda9-4629-bd41-a01b6195864a', }) - @ApiCustomResponse(UnifiedDepartmentOutput) + @ApiCustomResponse(UnifiedOrderOutput) @UseGuards(ApiKeyAuthGuard) @Get(':id') async retrieve( @@ -110,7 +103,7 @@ export class DepartmentController { await this.connectionUtils.getConnectionMetadataFromConnectionToken( connection_token, ); - return this.departmentService.getDepartment( + return this.orderService.getOrder( id, linkedUserId, remoteSource, diff --git a/packages/api/src/ecommerce/order/order.module.ts b/packages/api/src/ecommerce/order/order.module.ts index 9d60d9594..a73a7e6d0 100644 --- a/packages/api/src/ecommerce/order/order.module.ts +++ b/packages/api/src/ecommerce/order/order.module.ts @@ -1,41 +1,31 @@ -import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; +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 { ConnectionUtils } from '@@core/connections/@utils'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { Utils } from '@ecommerce/@lib/@utils'; import { Module } from '@nestjs/common'; -import { DepartmentController } from './department.controller'; -import { DepartmentService } from './services/department.service'; +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 { SyncService } from './sync/sync.service'; -import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { AshbyService } from './services/ashby'; - -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; - -import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; -import { AshbyDepartmentMapper } from './services/ashby/mappers'; -import { Utils } from '@ats/@lib/@utils'; @Module({ imports: [BullQueueModule], - controllers: [DepartmentController], + controllers: [OrderController], providers: [ - DepartmentService, + OrderService, CoreUnification, - SyncService, WebhookService, - ServiceRegistry, - IngestDataService, - Utils, - AshbyDepartmentMapper, + ShopifyOrderMapper, /* PROVIDERS SERVICES */ - AshbyService, + ShopifyService, ], exports: [SyncService], }) -export class DepartmentModule {} +export class OrderModule {} diff --git a/packages/api/src/ecommerce/order/services/ashby/mappers.ts b/packages/api/src/ecommerce/order/services/ashby/mappers.ts deleted file mode 100644 index 877efdf88..000000000 --- a/packages/api/src/ecommerce/order/services/ashby/mappers.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { AshbyDepartmentInput, AshbyDepartmentOutput } from './types'; -import { - UnifiedDepartmentInput, - UnifiedDepartmentOutput, -} from '@ats/department/types/model.unified'; -import { IDepartmentMapper } from '@ats/department/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 '@ats/@lib/@utils'; - -@Injectable() -export class AshbyDepartmentMapper implements IDepartmentMapper { - constructor( - private mappersRegistry: MappersRegistry, - private utils: Utils, - private coreUnificationService: CoreUnification, - ) { - this.mappersRegistry.registerService('ats', 'department', 'ashby', this); - } - - async desunify( - source: UnifiedDepartmentInput, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): Promise { - return; - } - - async unify( - source: AshbyDepartmentOutput | AshbyDepartmentOutput[], - connectionId: string, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): Promise { - if (!Array.isArray(source)) { - return await this.mapSingleDepartmentToUnified( - source, - connectionId, - customFieldMappings, - ); - } - // Handling array of AshbyDepartmentOutput - return Promise.all( - source.map((department) => - this.mapSingleDepartmentToUnified( - department, - connectionId, - customFieldMappings, - ), - ), - ); - } - - private async mapSingleDepartmentToUnified( - department: AshbyDepartmentOutput, - connectionId: string, - customFieldMappings?: { - slug: string; - remote_id: string; - }[], - ): Promise { - return { - remote_id: department.id, - remote_data: department, - name: department.name || null, - }; - } -} diff --git a/packages/api/src/ecommerce/order/services/ashby/types.ts b/packages/api/src/ecommerce/order/services/ashby/types.ts deleted file mode 100644 index d3c47509a..000000000 --- a/packages/api/src/ecommerce/order/services/ashby/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface AshbyDepartmentInput { - id: string; - name: string; - isArchived: boolean; - parentId: string; -} - -export type AshbyDepartmentOutput = Partial; diff --git a/packages/api/src/ecommerce/fulfillmentorders/services/department.service.ts b/packages/api/src/ecommerce/order/services/order.service.ts similarity index 63% rename from packages/api/src/ecommerce/fulfillmentorders/services/department.service.ts rename to packages/api/src/ecommerce/order/services/order.service.ts index deb50851d..9f91f66d7 100644 --- a/packages/api/src/ecommerce/fulfillmentorders/services/department.service.ts +++ b/packages/api/src/ecommerce/order/services/order.service.ts @@ -2,36 +2,36 @@ import { Injectable } from '@nestjs/common'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; -import { UnifiedDepartmentOutput } from '../types/model.unified'; +import { UnifiedOrderOutput } from '../types/model.unified'; @Injectable() -export class DepartmentService { +export class OrderService { constructor(private prisma: PrismaService, private logger: LoggerService) { - this.logger.setContext(DepartmentService.name); + this.logger.setContext(OrderService.name); } - async getDepartment( - id_ats_department: string, + async getOrder( + id_ecom_order: string, linkedUserId: string, integrationId: string, remote_data?: boolean, - ): Promise { + ): Promise { try { - const department = await this.prisma.ats_departments.findUnique({ + const order = await this.prisma.ecom_orders.findUnique({ where: { - id_ats_department: id_ats_department, + id_ecom_order: id_ecom_order, }, }); - if (!department) { - throw new Error(`Department with ID ${id_ats_department} not found.`); + if (!order) { + throw new Error(`Order with ID ${id_ecom_order} not found.`); } - // Fetch field mappings for the department + // Fetch field mappings for the order const values = await this.prisma.value.findMany({ where: { entity: { - ressource_owner_id: department.id_ats_department, + ressource_owner_id: order.id_ecom_order, }, }, include: { @@ -51,21 +51,21 @@ export class DepartmentService { [key]: value, })); - // Transform to UnifiedDepartmentOutput format - const unifiedDepartment: UnifiedDepartmentOutput = { - id: department.id_ats_department, - name: department.name, + // Transform to UnifiedOrderOutput format + const unifiedOrder: UnifiedOrderOutput = { + id: order.id_ecom_order, + name: order.name, field_mappings: field_mappings, - remote_id: department.remote_id, - created_at: department.created_at, - modified_at: department.modified_at, + remote_id: order.remote_id, + created_at: order.created_at, + modified_at: order.modified_at, }; - let res: UnifiedDepartmentOutput = unifiedDepartment; + let res: UnifiedOrderOutput = unifiedOrder; if (remote_data) { const resp = await this.prisma.remote_data.findFirst({ where: { - ressource_owner_id: department.id_ats_department, + ressource_owner_id: order.id_ecom_order, }, }); const remote_data = JSON.parse(resp.data); @@ -79,9 +79,9 @@ export class DepartmentService { data: { id_event: uuidv4(), status: 'success', - type: 'ats.department.pull', + type: 'ecommerce.order.pull', method: 'GET', - url: '/ats/department', + url: '/ecommerce/order', provider: integrationId, direction: '0', timestamp: new Date(), @@ -95,7 +95,7 @@ export class DepartmentService { } } - async getDepartments( + async getOrders( connection_id: string, integrationId: string, linkedUserId: string, @@ -103,7 +103,7 @@ export class DepartmentService { remote_data?: boolean, cursor?: string, ): Promise<{ - data: UnifiedDepartmentOutput[]; + data: UnifiedOrderOutput[]; prev_cursor: null | string; next_cursor: null | string; }> { @@ -112,10 +112,10 @@ export class DepartmentService { let next_cursor = null; if (cursor) { - const isCursorPresent = await this.prisma.ats_departments.findFirst({ + const isCursorPresent = await this.prisma.ecom_orders.findFirst({ where: { id_connection: connection_id, - id_ats_department: cursor, + id_ecom_order: cursor, }, }); if (!isCursorPresent) { @@ -123,11 +123,11 @@ export class DepartmentService { } } - const departments = await this.prisma.ats_departments.findMany({ + const orders = await this.prisma.ecom_orders.findMany({ take: limit + 1, cursor: cursor ? { - id_ats_department: cursor, + id_ecom_order: cursor, } : undefined, orderBy: { @@ -138,24 +138,24 @@ export class DepartmentService { }, }); - if (departments.length === limit + 1) { + if (orders.length === limit + 1) { next_cursor = Buffer.from( - departments[departments.length - 1].id_ats_department, + orders[orders.length - 1].id_ecom_order, ).toString('base64'); - departments.pop(); + orders.pop(); } if (cursor) { prev_cursor = Buffer.from(cursor).toString('base64'); } - const unifiedDepartments: UnifiedDepartmentOutput[] = await Promise.all( - departments.map(async (department) => { - // Fetch field mappings for the department + const unifiedOrders: UnifiedOrderOutput[] = await Promise.all( + orders.map(async (order) => { + // Fetch field mappings for the order const values = await this.prisma.value.findMany({ where: { entity: { - ressource_owner_id: department.id_ats_department, + ressource_owner_id: order.id_ecom_order, }, }, include: { @@ -178,30 +178,30 @@ export class DepartmentService { }), ); - // Transform to UnifiedDepartmentOutput format + // Transform to UnifiedOrderOutput format return { - id: department.id_ats_department, - name: department.name, + id: order.id_ecom_order, + name: order.name, field_mappings: field_mappings, - remote_id: department.remote_id, - created_at: department.created_at, - modified_at: department.modified_at, + remote_id: order.remote_id, + created_at: order.created_at, + modified_at: order.modified_at, }; }), ); - let res: UnifiedDepartmentOutput[] = unifiedDepartments; + let res: UnifiedOrderOutput[] = unifiedOrders; if (remote_data) { - const remote_array_data: UnifiedDepartmentOutput[] = await Promise.all( - res.map(async (department) => { + const remote_array_data: UnifiedOrderOutput[] = await Promise.all( + res.map(async (order) => { const resp = await this.prisma.remote_data.findFirst({ where: { - ressource_owner_id: department.id, + ressource_owner_id: order.id, }, }); const remote_data = JSON.parse(resp.data); - return { ...department, remote_data }; + return { ...order, remote_data }; }), ); @@ -212,9 +212,9 @@ export class DepartmentService { data: { id_event: uuidv4(), status: 'success', - type: 'ats.department.pull', + type: 'ecommerce.order.pull', method: 'GET', - url: '/ats/departments', + url: '/ecommerce/orders', provider: integrationId, direction: '0', timestamp: new Date(), diff --git a/packages/api/src/ecommerce/order/services/registry.service.ts b/packages/api/src/ecommerce/order/services/registry.service.ts index ee946e7b7..33669ca45 100644 --- a/packages/api/src/ecommerce/order/services/registry.service.ts +++ b/packages/api/src/ecommerce/order/services/registry.service.ts @@ -1,19 +1,19 @@ import { Injectable } from '@nestjs/common'; -import { IDepartmentService } from '../types'; +import { IOrderService } from '../types'; @Injectable() export class ServiceRegistry { - private serviceMap: Map; + private serviceMap: Map; constructor() { - this.serviceMap = new Map(); + this.serviceMap = new Map(); } - registerService(serviceKey: string, service: IDepartmentService) { + registerService(serviceKey: string, service: IOrderService) { this.serviceMap.set(serviceKey, service); } - getService(integrationId: string): IDepartmentService { + getService(integrationId: string): IOrderService { const service = this.serviceMap.get(integrationId); if (!service) { throw new ReferenceError( diff --git a/packages/api/src/ecommerce/fulfillment/services/ashby/index.ts b/packages/api/src/ecommerce/order/services/shopify/index.ts similarity index 61% rename from packages/api/src/ecommerce/fulfillment/services/ashby/index.ts rename to packages/api/src/ecommerce/order/services/shopify/index.ts index cceda214e..6edc0b839 100644 --- a/packages/api/src/ecommerce/fulfillment/services/ashby/index.ts +++ b/packages/api/src/ecommerce/order/services/shopify/index.ts @@ -1,20 +1,19 @@ -import { Injectable } from '@nestjs/common'; -import { IDepartmentService } from '@ats/department/types'; -import { AtsObject } from '@ats/@lib/@types'; -import axios from 'axios'; -import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; -import { ActionType, handle3rdPartyServiceError } from '@@core/utils/errors'; 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 { ServiceRegistry } from '../registry.service'; -import { AshbyDepartmentInput, AshbyDepartmentOutput } from './types'; import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { OriginalDepartmentOutput } from '@@core/utils/types/original/original.ats'; import { SyncParam } from '@@core/utils/types/interface'; +import { OriginalOrderOutput } from '@@core/utils/types/original/original.ecommerce'; +import { IOrderService } from '@ecommerce/order/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { ShopifyOrderOutput } from './types'; +import { EcommerceObject } from '@panora/shared'; @Injectable() -export class AshbyService implements IDepartmentService { +export class ShopifyService implements IOrderService { constructor( private prisma: PrismaService, private logger: LoggerService, @@ -22,26 +21,26 @@ export class AshbyService implements IDepartmentService { private registry: ServiceRegistry, ) { this.logger.setContext( - AtsObject.department.toUpperCase() + ':' + AshbyService.name, + EcommerceObject.order.toUpperCase() + ':' + ShopifyService.name, ); - this.registry.registerService('ashby', this); + this.registry.registerService('shopify', this); } - addDepartment( - departmentData: DesunifyReturnType, + addOrder( + orderData: DesunifyReturnType, linkedUserId: string, - ): Promise> { + ): Promise> { throw new Error('Method not implemented.'); } - async sync(data: SyncParam): Promise> { + async sync(data: SyncParam): Promise> { try { const { linkedUserId } = data; const connection = await this.prisma.connections.findFirst({ where: { id_linked_user: linkedUserId, - provider_slug: 'ashby', - vertical: 'ats', + provider_slug: 'shopify', + vertical: 'ecommerce', }, }); const resp = await axios.post( @@ -55,12 +54,12 @@ export class AshbyService implements IDepartmentService { }, }, ); - const departments: AshbyDepartmentOutput[] = resp.data.results; - this.logger.log(`Synced ashby departments !`); + const orders: ShopifyOrderOutput[] = resp.data.results; + this.logger.log(`Synced shopify orders !`); return { - data: departments, - message: 'Ashby departments retrieved', + data: orders, + message: 'Ashby orders retrieved', statusCode: 200, }; } catch (error) { diff --git a/packages/api/src/ecommerce/order/services/shopify/mappers.ts b/packages/api/src/ecommerce/order/services/shopify/mappers.ts new file mode 100644 index 000000000..70dc50e7e --- /dev/null +++ b/packages/api/src/ecommerce/order/services/shopify/mappers.ts @@ -0,0 +1,69 @@ +import { + UnifiedOrderInput, + UnifiedOrderOutput, +} 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 { ShopifyOrderOutput } from './types'; + +@Injectable() +export class ShopifyOrderMapper implements IOrderMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService('ecommerce', 'order', 'shopify', this); + } + + async desunify( + source: UnifiedOrderInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + return; + } + + async unify( + source: ShopifyOrderOutput | ShopifyOrderOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return await this.mapSingleOrderToUnified( + source, + connectionId, + customFieldMappings, + ); + } + // Handling array of AshbyOrderOutput + return Promise.all( + source.map((order) => + this.mapSingleOrderToUnified(order, connectionId, customFieldMappings), + ), + ); + } + + private async mapSingleOrderToUnified( + order: ShopifyOrderOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + return { + remote_id: order.id, + remote_data: order, + name: order.name || null, + }; + } +} diff --git a/packages/api/src/ecommerce/order/services/shopify/types.ts b/packages/api/src/ecommerce/order/services/shopify/types.ts new file mode 100644 index 000000000..b83667c5f --- /dev/null +++ b/packages/api/src/ecommerce/order/services/shopify/types.ts @@ -0,0 +1,5 @@ +export interface ShopifyOrderInput { + [key: string]: any; +} + +export type ShopifyOrderOutput = Partial; diff --git a/packages/api/src/ecommerce/order/sync/sync.processor.ts b/packages/api/src/ecommerce/order/sync/sync.processor.ts index 4c01a02f0..c3d981d5c 100644 --- a/packages/api/src/ecommerce/order/sync/sync.processor.ts +++ b/packages/api/src/ecommerce/order/sync/sync.processor.ts @@ -7,13 +7,13 @@ import { Queues } from '@@core/@core-services/queues/types'; export class SyncProcessor { constructor(private syncService: SyncService) {} - @Process('ats-sync-departments') - async handleSyncDepartments(job: Job) { + @Process('ecommerce-sync-orders') + async handleSyncOrders(job: Job) { try { - console.log(`Processing queue -> ats-sync-departments ${job.id}`); + console.log(`Processing queue -> ecommerce-sync-orders ${job.id}`); await this.syncService.kickstartSync(); } catch (error) { - console.error('Error syncing ats departments', error); + console.error('Error syncing ecommerce orders', error); } } } diff --git a/packages/api/src/ecommerce/order/sync/sync.service.ts b/packages/api/src/ecommerce/order/sync/sync.service.ts index 61c7cbe95..53b97b19e 100644 --- a/packages/api/src/ecommerce/order/sync/sync.service.ts +++ b/packages/api/src/ecommerce/order/sync/sync.service.ts @@ -1,23 +1,21 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { BullQueueService } from '@@core/@core-services/queues/shared.service'; +import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; +import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; +import { OriginalOrderOutput } from '@@core/utils/types/original/original.ecommerce'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; +import { ECOMMERCE_PROVIDERS } from '@panora/shared'; +import { ecom_orders as EcommerceOrder } from '@prisma/client'; import { v4 as uuidv4 } from 'uuid'; -import { FieldMappingService } from '@@core/field-mapping/field-mapping.service'; import { ServiceRegistry } from '../services/registry.service'; -import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; -import { CoreSyncRegistry } from '@@core/@core-services/registries/core-sync.registry'; -import { ApiResponse } from '@@core/utils/types'; -import { IDepartmentService } from '../types'; -import { OriginalDepartmentOutput } from '@@core/utils/types/original/original.ats'; -import { UnifiedDepartmentOutput } from '../types/model.unified'; -import { ats_departments as AtsDepartment } from '@prisma/client'; -import { ATS_PROVIDERS } from '@panora/shared'; -import { AtsObject } from '@ats/@lib/@types'; -import { BullQueueService } from '@@core/@core-services/queues/shared.service'; -import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; -import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; -import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IOrderService } from '../types'; +import { UnifiedOrderOutput } from '../types/model.unified'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { @@ -33,13 +31,13 @@ export class SyncService implements OnModuleInit, IBaseSync { private ingestService: IngestDataService, ) { this.logger.setContext(SyncService.name); - this.registry.registerService('ats', 'department', this); + this.registry.registerService('ecommerce', 'order', this); } async onModuleInit() { try { await this.bullQueueService.queueSyncJob( - 'ats-sync-departments', + 'ecommerce-sync-orders', '0 0 * * *', ); } catch (error) { @@ -50,7 +48,7 @@ export class SyncService implements OnModuleInit, IBaseSync { @Cron('0 */8 * * *') // every 8 hours async kickstartSync(user_id?: string) { try { - this.logger.log('Syncing departments...'); + this.logger.log('Syncing orders...'); const users = user_id ? [ await this.prisma.users.findUnique({ @@ -76,7 +74,7 @@ export class SyncService implements OnModuleInit, IBaseSync { }); linkedUsers.map(async (linkedUser) => { try { - const providers = ATS_PROVIDERS; + const providers = ECOMMERCE_PROVIDERS; for (const provider of providers) { try { await this.syncForLinkedUser({ @@ -102,15 +100,15 @@ export class SyncService implements OnModuleInit, IBaseSync { async syncForLinkedUser(param: SyncLinkedUserType) { try { const { integrationId, linkedUserId } = param; - const service: IDepartmentService = + const service: IOrderService = this.serviceRegistry.getService(integrationId); if (!service) return; await this.ingestService.syncForLinkedUser< - UnifiedDepartmentOutput, - OriginalDepartmentOutput, - IDepartmentService - >(integrationId, linkedUserId, 'ats', 'department', service, []); + UnifiedOrderOutput, + OriginalOrderOutput, + IOrderService + >(integrationId, linkedUserId, 'ecommerce', 'order', service, []); } catch (error) { throw error; } @@ -119,27 +117,27 @@ export class SyncService implements OnModuleInit, IBaseSync { async saveToDb( connection_id: string, linkedUserId: string, - departments: UnifiedDepartmentOutput[], + orders: UnifiedOrderOutput[], originSource: string, remote_data: Record[], - ): Promise { + ): Promise { try { - const departments_results: AtsDepartment[] = []; + const orders_results: EcommerceOrder[] = []; - const updateOrCreateDepartment = async ( - department: UnifiedDepartmentOutput, + const updateOrCreateOrder = async ( + order: UnifiedOrderOutput, originId: string, ) => { - let existingDepartment; + let existingOrder; if (!originId) { - existingDepartment = await this.prisma.ats_departments.findFirst({ + existingOrder = await this.prisma.ecom_orders.findFirst({ where: { - name: department.name, + name: order.name, id_connection: connection_id, }, }); } else { - existingDepartment = await this.prisma.ats_departments.findFirst({ + existingOrder = await this.prisma.ecom_orders.findFirst({ where: { remote_id: originId, id_connection: connection_id, @@ -148,22 +146,22 @@ export class SyncService implements OnModuleInit, IBaseSync { } const baseData: any = { - name: department.name ?? null, + name: order.name ?? null, modified_at: new Date(), }; - if (existingDepartment) { - return await this.prisma.ats_departments.update({ + if (existingOrder) { + return await this.prisma.ecom_orders.update({ where: { - id_ats_department: existingDepartment.id_ats_department, + id_ecom_order: existingOrder.id_ecom_order, }, data: baseData, }); } else { - return await this.prisma.ats_departments.create({ + return await this.prisma.ecom_orders.create({ data: { ...baseData, - id_ats_department: uuidv4(), + id_ecom_order: uuidv4(), created_at: new Date(), remote_id: originId, id_connection: connection_id, @@ -172,30 +170,27 @@ export class SyncService implements OnModuleInit, IBaseSync { } }; - for (let i = 0; i < departments.length; i++) { - const department = departments[i]; - const originId = department.remote_id; + for (let i = 0; i < orders.length; i++) { + const order = orders[i]; + const originId = order.remote_id; - const res = await updateOrCreateDepartment(department, originId); - const department_id = res.id_ats_department; - departments_results.push(res); + const res = await updateOrCreateOrder(order, originId); + const order_id = res.id_ecom_order; + orders_results.push(res); // Process field mappings await this.ingestService.processFieldMappings( - department.field_mappings, - department_id, + order.field_mappings, + order_id, originSource, linkedUserId, ); // Process remote data - await this.ingestService.processRemoteData( - department_id, - remote_data[i], - ); + await this.ingestService.processRemoteData(order_id, remote_data[i]); } - return departments_results; + return orders_results; } catch (error) { throw error; } diff --git a/packages/api/src/ecommerce/order/types/index.ts b/packages/api/src/ecommerce/order/types/index.ts index 2cca2d7d6..8a0625f06 100644 --- a/packages/api/src/ecommerce/order/types/index.ts +++ b/packages/api/src/ecommerce/order/types/index.ts @@ -1,24 +1,21 @@ import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; -import { - UnifiedDepartmentInput, - UnifiedDepartmentOutput, -} from './model.unified'; -import { OriginalDepartmentOutput } from '@@core/utils/types/original/original.ats'; +import { UnifiedOrderInput, UnifiedOrderOutput } 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'; -export interface IDepartmentService extends IBaseObjectService { - addDepartment( - departmentData: DesunifyReturnType, +export interface IOrderService extends IBaseObjectService { + addOrder( + orderData: DesunifyReturnType, linkedUserId: string, - ): Promise>; + ): Promise>; - sync(data: SyncParam): Promise>; + sync(data: SyncParam): Promise>; } -export interface IDepartmentMapper { +export interface IOrderMapper { desunify( - source: UnifiedDepartmentInput, + source: UnifiedOrderInput, customFieldMappings?: { slug: string; remote_id: string; @@ -26,11 +23,11 @@ export interface IDepartmentMapper { ): DesunifyReturnType; unify( - source: OriginalDepartmentOutput | OriginalDepartmentOutput[], + source: OriginalOrderOutput | OriginalOrderOutput[], connectionId: string, customFieldMappings?: { slug: string; remote_id: string; }[], - ): Promise; + ): Promise; } diff --git a/packages/api/src/ecommerce/order/types/model.unified.ts b/packages/api/src/ecommerce/order/types/model.unified.ts index a874dcf6b..1ffcee56d 100644 --- a/packages/api/src/ecommerce/order/types/model.unified.ts +++ b/packages/api/src/ecommerce/order/types/model.unified.ts @@ -1,61 +1,124 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsUUID, IsOptional, IsString, IsDateString } from 'class-validator'; +import { + IsUUID, + IsOptional, + IsString, + IsDateString, + IsInt, +} from 'class-validator'; -export class UnifiedDepartmentInput { +export class UnifiedOrderInput { @ApiPropertyOptional({ type: String, - description: 'The name of the department', + description: 'The status of the order', }) @IsString() @IsOptional() - name?: string; + order_status?: string; @ApiPropertyOptional({ - type: {}, - description: - 'The custom field mappings of the object between the remote 3rd party & Panora', + type: String, + description: 'The number of the order', }) + @IsString() @IsOptional() - field_mappings?: Record; -} + order_number?: string; -export class UnifiedDepartmentOutput extends UnifiedDepartmentInput { @ApiPropertyOptional({ type: String, - description: 'The UUID of the department', + description: 'The payment status of the order', }) - @IsUUID() + @IsString() + @IsOptional() + payment_status?: string; + + @ApiPropertyOptional({ + type: String, + description: 'The currency of the order', + }) + @IsString() + @IsOptional() + currency?: string; + + @ApiPropertyOptional({ + type: Number, + description: 'The total price of the order', + }) + @IsInt() + @IsOptional() + total_price?: number; + + @ApiPropertyOptional({ + type: Number, + description: 'The total discount on the order', + }) + @IsInt() + @IsOptional() + total_discount?: number; + + @ApiPropertyOptional({ + type: Number, + description: 'The total shipping cost of the order', + }) + @IsInt() + @IsOptional() + total_shipping?: number; + + @ApiPropertyOptional({ + type: Number, + description: 'The total tax on the order', + }) + @IsInt() @IsOptional() - id?: string; + total_tax?: number; @ApiPropertyOptional({ type: String, - description: - 'The remote ID of the department in the context of the 3rd Party', + description: 'The fulfillment status of the order', }) @IsString() @IsOptional() - remote_id?: string; + fulfillment_status?: string; + + @ApiPropertyOptional({ + type: String, + description: 'The UUID of the customer associated with the order', + }) + @IsUUID() + @IsOptional() + customer_id?: string; +} +export class UnifiedOrderOutput extends UnifiedOrderInput { @ApiPropertyOptional({ - type: {}, - description: - 'The remote data of the department in the context of the 3rd Party', + type: String, + description: 'The UUID of the order', }) + @IsUUID() @IsOptional() - remote_data?: Record; + id: string; @ApiPropertyOptional({ - type: {}, + type: String, + description: 'The remote ID of the order in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id: string; + + @ApiPropertyOptional({ + type: String, description: 'The created date of the object', }) + @IsDateString() @IsOptional() - created_at?: any; + created_at?: string; @ApiPropertyOptional({ - type: {}, + type: String, description: 'The modified date of the object', }) + @IsDateString() @IsOptional() - modified_at?: any; + modified_at?: string; } diff --git a/packages/api/src/ecommerce/product/product.controller.ts b/packages/api/src/ecommerce/product/product.controller.ts index c36787e98..129c0e57c 100644 --- a/packages/api/src/ecommerce/product/product.controller.ts +++ b/packages/api/src/ecommerce/product/product.controller.ts @@ -1,32 +1,25 @@ +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +import { ApiCustomResponse } from '@@core/utils/types'; import { Controller, - Post, - Body, - Query, Get, - Patch, - Param, Headers, + Param, + Query, UseGuards, } from '@nestjs/common'; -import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { - ApiBody, + ApiHeader, ApiOperation, ApiParam, ApiQuery, ApiTags, - ApiHeader, } from '@nestjs/swagger'; -import { ApiCustomResponse } from '@@core/utils/types'; import { ProductService } from './services/product.service'; -import { - UnifiedProductInput, - UnifiedProductOutput, -} from './types/model.unified'; -import { ConnectionUtils } from '@@core/connections/@utils'; -import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard'; -import { FetchObjectsQueryDto } from '@@core/utils/dtos/fetch-objects-query.dto'; +import { UnifiedProductOutput } from './types/model.unified'; @ApiTags('ecommerce/product') @Controller('ecommerce/product') diff --git a/packages/api/src/ecommerce/product/services/product.service.ts b/packages/api/src/ecommerce/product/services/product.service.ts index 5f0ff9e7f..4cae971a5 100644 --- a/packages/api/src/ecommerce/product/services/product.service.ts +++ b/packages/api/src/ecommerce/product/services/product.service.ts @@ -3,6 +3,7 @@ import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { v4 as uuidv4 } from 'uuid'; import { UnifiedProductOutput } from '../types/model.unified'; +import { ecom_products as EcommerceProduct } from '@prisma/client'; @Injectable() export class ProductService { @@ -19,7 +20,7 @@ export class ProductService { try { const product = await this.prisma.ecom_products.findUnique({ where: { - id_ecommerce_product: id_ecommerce_product, + id_ecom_product: id_ecommerce_product, }, }); @@ -31,7 +32,7 @@ export class ProductService { const values = await this.prisma.value.findMany({ where: { entity: { - ressource_owner_id: product.id_ecommerce_product, + ressource_owner_id: product.id_ecom_product, }, }, include: { @@ -53,7 +54,7 @@ export class ProductService { // Transform to UnifiedProductOutput format const unifiedProduct: UnifiedProductOutput = { - id: product.id_ecommerce_product, + id: product.id_ecom_product, name: product.name, field_mappings: field_mappings, remote_id: product.remote_id, @@ -65,7 +66,7 @@ export class ProductService { if (remote_data) { const resp = await this.prisma.remote_data.findFirst({ where: { - ressource_owner_id: product.id_ecommerce_product, + ressource_owner_id: product.id_ecom_product, }, }); const remote_data = JSON.parse(resp.data); @@ -115,7 +116,7 @@ export class ProductService { const isCursorPresent = await this.prisma.ecom_products.findFirst({ where: { id_connection: connection_id, - id_ecommerce_product: cursor, + id_ecom_product: cursor, }, }); if (!isCursorPresent) { @@ -127,7 +128,7 @@ export class ProductService { take: limit + 1, cursor: cursor ? { - id_ecommerce_product: cursor, + id_ecom_product: cursor, } : undefined, orderBy: { @@ -140,7 +141,7 @@ export class ProductService { if (products.length === limit + 1) { next_cursor = Buffer.from( - products[products.length - 1].id_ecommerce_product, + products[products.length - 1].id_ecom_product, ).toString('base64'); products.pop(); } @@ -155,7 +156,7 @@ export class ProductService { const values = await this.prisma.value.findMany({ where: { entity: { - ressource_owner_id: product.id_ecommerce_product, + ressource_owner_id: product.id_ecom_product, }, }, include: { @@ -180,7 +181,7 @@ export class ProductService { // Transform to UnifiedProductOutput format return { - id: product.id_ecommerce_product, + id: product.id_ecom_product, name: product.name, field_mappings: field_mappings, remote_id: product.remote_id, diff --git a/packages/api/src/ecommerce/product/services/shopify/index.ts b/packages/api/src/ecommerce/product/services/shopify/index.ts index 5e31d0a75..4122b0128 100644 --- a/packages/api/src/ecommerce/product/services/shopify/index.ts +++ b/packages/api/src/ecommerce/product/services/shopify/index.ts @@ -10,6 +10,7 @@ import { Injectable } from '@nestjs/common'; import axios from 'axios'; import { ServiceRegistry } from '../registry.service'; import { ShopifyProductOutput } from './types'; +import { EcommerceObject } from '@panora/shared'; @Injectable() export class ShopifyService implements IProductService { diff --git a/packages/api/src/ecommerce/product/services/shopify/types.ts b/packages/api/src/ecommerce/product/services/shopify/types.ts index 7f546791b..b8ce967e2 100644 --- a/packages/api/src/ecommerce/product/services/shopify/types.ts +++ b/packages/api/src/ecommerce/product/services/shopify/types.ts @@ -1,5 +1,5 @@ export interface ShopifyProductInput { - id: string; + [key: string]: any; } export type ShopifyProductOutput = Partial; diff --git a/packages/api/src/ecommerce/product/sync/sync.service.ts b/packages/api/src/ecommerce/product/sync/sync.service.ts index 8afd0a4c4..61ade5650 100644 --- a/packages/api/src/ecommerce/product/sync/sync.service.ts +++ b/packages/api/src/ecommerce/product/sync/sync.service.ts @@ -10,13 +10,12 @@ import { IBaseSync, SyncLinkedUserType } from '@@core/utils/types/interface'; import { OriginalProductOutput } from '@@core/utils/types/original/original.ecommerce'; import { Injectable, OnModuleInit } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; -import { ATS_PROVIDERS } from '@panora/shared'; -import { ecommerce_products as EcommerceProduct } from '@prisma/client'; +import { ATS_PROVIDERS, ECOMMERCE_PROVIDERS } from '@panora/shared'; import { v4 as uuidv4 } from 'uuid'; import { ServiceRegistry } from '../services/registry.service'; import { IProductService } from '../types'; import { UnifiedProductOutput } from '../types/model.unified'; - +import { ecom_products as EcommerceProduct } from '@prisma/client'; @Injectable() export class SyncService implements OnModuleInit, IBaseSync { constructor( @@ -74,7 +73,7 @@ export class SyncService implements OnModuleInit, IBaseSync { }); linkedUsers.map(async (linkedUser) => { try { - const providers = ATS_PROVIDERS; + const providers = ECOMMERCE_PROVIDERS; for (const provider of providers) { try { await this.syncForLinkedUser({ @@ -153,7 +152,7 @@ export class SyncService implements OnModuleInit, IBaseSync { if (existingProduct) { return await this.prisma.ecom_products.update({ where: { - id_ecommerce_product: existingProduct.id_ecommerce_product, + id_ecom_product: existingProduct.id_ecom_product, }, data: baseData, }); @@ -161,7 +160,7 @@ export class SyncService implements OnModuleInit, IBaseSync { return await this.prisma.ecom_products.create({ data: { ...baseData, - id_ecommerce_product: uuidv4(), + id_ecom_product: uuidv4(), created_at: new Date(), remote_id: originId, id_connection: connection_id, @@ -175,7 +174,7 @@ export class SyncService implements OnModuleInit, IBaseSync { const originId = product.remote_id; const res = await updateOrCreateProduct(product, originId); - const product_id = res.id_ecommerce_product; + const product_id = res.id_ecom_product; products_results.push(res); // Process field mappings diff --git a/packages/api/src/ecommerce/product/types/model.unified.ts b/packages/api/src/ecommerce/product/types/model.unified.ts index 813af5d6a..e8136fd0e 100644 --- a/packages/api/src/ecommerce/product/types/model.unified.ts +++ b/packages/api/src/ecommerce/product/types/model.unified.ts @@ -1,53 +1,116 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsUUID, IsOptional, IsString, IsDateString } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsArray, + IsDateString, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; export class UnifiedProductInput { @ApiPropertyOptional({ - type: {}, - description: - 'The custom field mappings of the object between the remote 3rd party & Panora', + type: String, + description: 'The URL of the product', }) + @IsString() @IsOptional() - field_mappings?: Record; -} + product_url?: string; -export class UnifiedProductOutput extends UnifiedProductInput { @ApiPropertyOptional({ type: String, - description: 'The UUID of the department', + description: 'The type of the product', }) - @IsUUID() + @IsString() + @IsOptional() + product_type?: string; + + @ApiPropertyOptional({ + type: String, + description: 'The status of the product', + }) + @IsString() + @IsOptional() + product_status?: string; + + @ApiPropertyOptional({ + type: [String], + description: 'The URLs of the product images', + }) + @IsArray() + @IsOptional() + @IsString({ each: true }) + images_urls?: string[]; + + @ApiPropertyOptional({ + type: String, + description: 'The description of the product', + }) + @IsString() @IsOptional() - id?: string; + description?: string; @ApiPropertyOptional({ type: String, - description: - 'The remote ID of the department in the context of the 3rd Party', + description: 'The vendor of the product', }) @IsString() @IsOptional() - remote_id?: string; + vendor?: string; @ApiPropertyOptional({ - type: {}, - description: - 'The remote data of the department in the context of the 3rd Party', + type: [Object], + description: 'The variants of the product', }) @IsOptional() - remote_data?: Record; + variants?: { + title: string; + price: number; + sku: string; + options: any; + weight: number; + inventory_quantity: number; + }[]; @ApiPropertyOptional({ - type: {}, + type: [String], + description: 'The tags associated with the product', + }) + @IsArray() + @IsOptional() + @IsString({ each: true }) + tags?: string[]; +} + +export class UnifiedProductOutput extends UnifiedProductInput { + @ApiPropertyOptional({ + type: String, + description: 'The UUID of the product', + }) + @IsUUID() + @IsOptional() + id: string; + + @ApiPropertyOptional({ + type: String, + description: 'The remote ID of the product in the context of the 3rd Party', + }) + @IsString() + @IsOptional() + remote_id: string; + + @ApiPropertyOptional({ + type: String, description: 'The created date of the object', }) + @IsDateString() @IsOptional() - created_at?: any; + created_at?: string; @ApiPropertyOptional({ - type: {}, + type: String, description: 'The modified date of the object', }) + @IsDateString() @IsOptional() - modified_at?: any; + modified_at?: string; } diff --git a/packages/api/swagger/swagger-spec.json b/packages/api/swagger/swagger-spec.json index 9b3bf9c0a..8c457250f 100644 --- a/packages/api/swagger/swagger-spec.json +++ b/packages/api/swagger/swagger-spec.json @@ -14294,6 +14294,671 @@ ] } }, + "/ecommerce/product": { + "get": { + "operationId": "getProducts", + "summary": "List a batch of Products", + "parameters": [ + { + "name": "x-connection-token", + "required": true, + "in": "header", + "description": "The connection token", + "schema": { + "type": "string" + } + }, + { + "name": "remote_data", + "required": false, + "in": "query", + "description": "Set to true to include data from the original software.", + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/UnifiedProductOutput" + } + } + } + ] + } + } + } + } + }, + "tags": [ + "ecommerce/product" + ] + } + }, + "/ecommerce/product/{id}": { + "get": { + "operationId": "getProduct", + "summary": "Retrieve a Product", + "description": "Retrieve a product from any connected Ats software", + "parameters": [ + { + "name": "x-connection-token", + "required": true, + "in": "header", + "description": "The connection token", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "description": "id of the product you want to retrieve.", + "schema": { + "type": "string" + } + }, + { + "name": "remote_data", + "required": false, + "in": "query", + "description": "Set to true to include data from the original Ats software.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/UnifiedProductOutput" + } + } + } + ] + } + } + } + } + }, + "tags": [ + "ecommerce/product" + ] + } + }, + "/ecommerce/order": { + "get": { + "operationId": "getOrders", + "summary": "List a batch of Orders", + "parameters": [ + { + "name": "x-connection-token", + "required": true, + "in": "header", + "description": "The connection token", + "schema": { + "type": "string" + } + }, + { + "name": "remote_data", + "required": false, + "in": "query", + "description": "Set to true to include data from the original software.", + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/UnifiedOrderOutput" + } + } + } + ] + } + } + } + } + }, + "tags": [ + "ecommerce/order" + ] + } + }, + "/ecommerce/order/{id}": { + "get": { + "operationId": "getOrder", + "summary": "Retrieve a Order", + "description": "Retrieve a order from any connected Ats software", + "parameters": [ + { + "name": "x-connection-token", + "required": true, + "in": "header", + "description": "The connection token", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "description": "id of the order you want to retrieve.", + "schema": { + "type": "string" + } + }, + { + "name": "remote_data", + "required": false, + "in": "query", + "description": "Set to true to include data from the original Ats software.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/UnifiedOrderOutput" + } + } + } + ] + } + } + } + } + }, + "tags": [ + "ecommerce/order" + ] + } + }, + "/ecommerce/customer": { + "get": { + "operationId": "getCustomers", + "summary": "List a batch of Customers", + "parameters": [ + { + "name": "x-connection-token", + "required": true, + "in": "header", + "description": "The connection token", + "schema": { + "type": "string" + } + }, + { + "name": "remote_data", + "required": false, + "in": "query", + "description": "Set to true to include data from the original software.", + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/UnifiedCustomerOutput" + } + } + } + ] + } + } + } + } + }, + "tags": [ + "ecommerce/customer" + ] + } + }, + "/ecommerce/customer/{id}": { + "get": { + "operationId": "getCustomer", + "summary": "Retrieve a Customer", + "description": "Retrieve a customer from any connected Ats software", + "parameters": [ + { + "name": "x-connection-token", + "required": true, + "in": "header", + "description": "The connection token", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "description": "id of the customer you want to retrieve.", + "schema": { + "type": "string" + } + }, + { + "name": "remote_data", + "required": false, + "in": "query", + "description": "Set to true to include data from the original Ats software.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/UnifiedCustomerOutput" + } + } + } + ] + } + } + } + } + }, + "tags": [ + "ecommerce/customer" + ] + } + }, + "/ecommerce/fulfillment": { + "get": { + "operationId": "getFulfillments", + "summary": "List a batch of Fulfillments", + "parameters": [ + { + "name": "x-connection-token", + "required": true, + "in": "header", + "description": "The connection token", + "schema": { + "type": "string" + } + }, + { + "name": "remote_data", + "required": false, + "in": "query", + "description": "Set to true to include data from the original software.", + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/UnifiedFulfillmentOutput" + } + } + } + ] + } + } + } + } + }, + "tags": [ + "ecommerce/fulfillment" + ] + } + }, + "/ecommerce/fulfillment/{id}": { + "get": { + "operationId": "getFulfillment", + "summary": "Retrieve a Fulfillment", + "description": "Retrieve a fulfillment from any connected Ats software", + "parameters": [ + { + "name": "x-connection-token", + "required": true, + "in": "header", + "description": "The connection token", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "description": "id of the fulfillment you want to retrieve.", + "schema": { + "type": "string" + } + }, + { + "name": "remote_data", + "required": false, + "in": "query", + "description": "Set to true to include data from the original Ats software.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/UnifiedFulfillmentOutput" + } + } + } + ] + } + } + } + } + }, + "tags": [ + "ecommerce/fulfillment" + ] + } + }, + "/ats/fulfillmentorders": { + "get": { + "operationId": "getFulfillmentOrderss", + "summary": "List a batch of FulfillmentOrderss", + "parameters": [ + { + "name": "x-connection-token", + "required": true, + "in": "header", + "description": "The connection token", + "schema": { + "type": "string" + } + }, + { + "name": "remote_data", + "required": false, + "in": "query", + "description": "Set to true to include data from the original software.", + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Set to get the number of records.", + "schema": { + "default": 50, + "type": "number" + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "description": "Set to get the number of records after this cursor.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/UnifiedFulfillmentOrdersOutput" + } + } + } + ] + } + } + } + } + }, + "tags": [ + "ats/fulfillmentorders" + ] + } + }, + "/ats/fulfillmentorders/{id}": { + "get": { + "operationId": "getFulfillmentOrders", + "summary": "Retrieve a FulfillmentOrders", + "description": "Retrieve a fulfillmentorders from any connected Ecommerce software", + "parameters": [ + { + "name": "x-connection-token", + "required": true, + "in": "header", + "description": "The connection token", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "description": "id of the fulfillmentorders you want to retrieve.", + "schema": { + "type": "string" + } + }, + { + "name": "remote_data", + "required": false, + "in": "query", + "description": "Set to true to include data from the original Ecommerce software.", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResponse" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/UnifiedFulfillmentOrdersOutput" + } + } + } + ] + } + } + } + } + }, + "tags": [ + "ats/fulfillmentorders" + ] + } + }, "/ticketing/attachments": { "get": { "operationId": "getTicketingAttachments", @@ -17943,6 +18608,197 @@ "permission", "field_mappings" ] + }, + "UnifiedProductOutput": { + "type": "object", + "properties": { + "field_mappings": { + "type": "object", + "properties": {} + }, + "id": { + "type": "string", + "description": "The UUID of the department" + }, + "remote_id": { + "type": "string", + "description": "The remote ID of the department in the context of the 3rd Party" + }, + "remote_data": { + "type": "object", + "properties": {} + }, + "created_at": { + "type": "object", + "properties": {} + }, + "modified_at": { + "type": "object", + "properties": {} + } + }, + "required": [ + "field_mappings", + "remote_data", + "created_at", + "modified_at" + ] + }, + "UnifiedOrderOutput": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the order" + }, + "field_mappings": { + "type": "object", + "properties": {} + }, + "id": { + "type": "string", + "description": "The UUID of the order" + }, + "remote_id": { + "type": "string", + "description": "The remote ID of the order in the context of the 3rd Party" + }, + "remote_data": { + "type": "object", + "properties": {} + }, + "created_at": { + "type": "object", + "properties": {} + }, + "modified_at": { + "type": "object", + "properties": {} + } + }, + "required": [ + "field_mappings", + "remote_data", + "created_at", + "modified_at" + ] + }, + "UnifiedCustomerOutput": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the customer" + }, + "field_mappings": { + "type": "object", + "properties": {} + }, + "id": { + "type": "string", + "description": "The UUID of the customer" + }, + "remote_id": { + "type": "string", + "description": "The remote ID of the customer in the context of the 3rd Party" + }, + "remote_data": { + "type": "object", + "properties": {} + }, + "created_at": { + "type": "object", + "properties": {} + }, + "modified_at": { + "type": "object", + "properties": {} + } + }, + "required": [ + "field_mappings", + "remote_data", + "created_at", + "modified_at" + ] + }, + "UnifiedFulfillmentOutput": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the fulfillment" + }, + "field_mappings": { + "type": "object", + "properties": {} + }, + "id": { + "type": "string", + "description": "The UUID of the fulfillment" + }, + "remote_id": { + "type": "string", + "description": "The remote ID of the fulfillment in the context of the 3rd Party" + }, + "remote_data": { + "type": "object", + "properties": {} + }, + "created_at": { + "type": "object", + "properties": {} + }, + "modified_at": { + "type": "object", + "properties": {} + } + }, + "required": [ + "field_mappings", + "remote_data", + "created_at", + "modified_at" + ] + }, + "UnifiedFulfillmentOrdersOutput": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the fulfillmentorders" + }, + "field_mappings": { + "type": "object", + "properties": {} + }, + "id": { + "type": "string", + "description": "The UUID of the fulfillmentorders" + }, + "remote_id": { + "type": "string", + "description": "The remote ID of the fulfillmentorders in the context of the 3rd Party" + }, + "remote_data": { + "type": "object", + "properties": {} + }, + "created_at": { + "type": "object", + "properties": {} + }, + "modified_at": { + "type": "object", + "properties": {} + } + }, + "required": [ + "field_mappings", + "remote_data", + "created_at", + "modified_at" + ] } } } diff --git a/packages/api/swagger/swagger-spec.yaml b/packages/api/swagger/swagger-spec.yaml index 2678190c3..cf26f1211 100644 --- a/packages/api/swagger/swagger-spec.yaml +++ b/packages/api/swagger/swagger-spec.yaml @@ -8445,6 +8445,401 @@ paths: data: $ref: '#/components/schemas/UnifiedUserOutput' tags: *ref_106 + /ecommerce/product: + get: + operationId: getProducts + summary: List a batch of Products + parameters: + - name: x-connection-token + required: true + in: header + description: The connection token + schema: + type: string + - name: remote_data + required: false + in: query + description: Set to true to include data from the original software. + schema: + type: boolean + - name: limit + required: false + in: query + description: Set to get the number of records. + schema: + default: 50 + type: number + - name: cursor + required: false + in: query + description: Set to get the number of records after this cursor. + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - properties: + data: + $ref: '#/components/schemas/UnifiedProductOutput' + tags: &ref_107 + - ecommerce/product + /ecommerce/product/{id}: + get: + operationId: getProduct + summary: Retrieve a Product + description: Retrieve a product from any connected Ats software + parameters: + - name: x-connection-token + required: true + in: header + description: The connection token + schema: + type: string + - name: id + required: true + in: path + description: id of the product you want to retrieve. + schema: + type: string + - name: remote_data + required: false + in: query + description: Set to true to include data from the original Ats software. + schema: + type: boolean + responses: + '200': + description: '' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - properties: + data: + $ref: '#/components/schemas/UnifiedProductOutput' + tags: *ref_107 + /ecommerce/order: + get: + operationId: getOrders + summary: List a batch of Orders + parameters: + - name: x-connection-token + required: true + in: header + description: The connection token + schema: + type: string + - name: remote_data + required: false + in: query + description: Set to true to include data from the original software. + schema: + type: boolean + - name: limit + required: false + in: query + description: Set to get the number of records. + schema: + default: 50 + type: number + - name: cursor + required: false + in: query + description: Set to get the number of records after this cursor. + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - properties: + data: + $ref: '#/components/schemas/UnifiedOrderOutput' + tags: &ref_108 + - ecommerce/order + /ecommerce/order/{id}: + get: + operationId: getOrder + summary: Retrieve a Order + description: Retrieve a order from any connected Ats software + parameters: + - name: x-connection-token + required: true + in: header + description: The connection token + schema: + type: string + - name: id + required: true + in: path + description: id of the order you want to retrieve. + schema: + type: string + - name: remote_data + required: false + in: query + description: Set to true to include data from the original Ats software. + schema: + type: boolean + responses: + '200': + description: '' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - properties: + data: + $ref: '#/components/schemas/UnifiedOrderOutput' + tags: *ref_108 + /ecommerce/customer: + get: + operationId: getCustomers + summary: List a batch of Customers + parameters: + - name: x-connection-token + required: true + in: header + description: The connection token + schema: + type: string + - name: remote_data + required: false + in: query + description: Set to true to include data from the original software. + schema: + type: boolean + - name: limit + required: false + in: query + description: Set to get the number of records. + schema: + default: 50 + type: number + - name: cursor + required: false + in: query + description: Set to get the number of records after this cursor. + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - properties: + data: + $ref: '#/components/schemas/UnifiedCustomerOutput' + tags: &ref_109 + - ecommerce/customer + /ecommerce/customer/{id}: + get: + operationId: getCustomer + summary: Retrieve a Customer + description: Retrieve a customer from any connected Ats software + parameters: + - name: x-connection-token + required: true + in: header + description: The connection token + schema: + type: string + - name: id + required: true + in: path + description: id of the customer you want to retrieve. + schema: + type: string + - name: remote_data + required: false + in: query + description: Set to true to include data from the original Ats software. + schema: + type: boolean + responses: + '200': + description: '' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - properties: + data: + $ref: '#/components/schemas/UnifiedCustomerOutput' + tags: *ref_109 + /ecommerce/fulfillment: + get: + operationId: getFulfillments + summary: List a batch of Fulfillments + parameters: + - name: x-connection-token + required: true + in: header + description: The connection token + schema: + type: string + - name: remote_data + required: false + in: query + description: Set to true to include data from the original software. + schema: + type: boolean + - name: limit + required: false + in: query + description: Set to get the number of records. + schema: + default: 50 + type: number + - name: cursor + required: false + in: query + description: Set to get the number of records after this cursor. + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - properties: + data: + $ref: '#/components/schemas/UnifiedFulfillmentOutput' + tags: &ref_110 + - ecommerce/fulfillment + /ecommerce/fulfillment/{id}: + get: + operationId: getFulfillment + summary: Retrieve a Fulfillment + description: Retrieve a fulfillment from any connected Ats software + parameters: + - name: x-connection-token + required: true + in: header + description: The connection token + schema: + type: string + - name: id + required: true + in: path + description: id of the fulfillment you want to retrieve. + schema: + type: string + - name: remote_data + required: false + in: query + description: Set to true to include data from the original Ats software. + schema: + type: boolean + responses: + '200': + description: '' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - properties: + data: + $ref: '#/components/schemas/UnifiedFulfillmentOutput' + tags: *ref_110 + /ats/fulfillmentorders: + get: + operationId: getFulfillmentOrderss + summary: List a batch of FulfillmentOrderss + parameters: + - name: x-connection-token + required: true + in: header + description: The connection token + schema: + type: string + - name: remote_data + required: false + in: query + description: Set to true to include data from the original software. + schema: + type: boolean + - name: limit + required: false + in: query + description: Set to get the number of records. + schema: + default: 50 + type: number + - name: cursor + required: false + in: query + description: Set to get the number of records after this cursor. + schema: + type: string + responses: + '200': + description: '' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - properties: + data: + $ref: '#/components/schemas/UnifiedFulfillmentOrdersOutput' + tags: &ref_111 + - ats/fulfillmentorders + /ats/fulfillmentorders/{id}: + get: + operationId: getFulfillmentOrders + summary: Retrieve a FulfillmentOrders + description: Retrieve a fulfillmentorders from any connected Ecommerce software + parameters: + - name: x-connection-token + required: true + in: header + description: The connection token + schema: + type: string + - name: id + required: true + in: path + description: id of the fulfillmentorders you want to retrieve. + schema: + type: string + - name: remote_data + required: false + in: query + description: Set to true to include data from the original Ecommerce software. + schema: + type: boolean + responses: + '200': + description: '' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/ApiResponse' + - properties: + data: + $ref: '#/components/schemas/UnifiedFulfillmentOrdersOutput' + tags: *ref_111 /ticketing/attachments: get: operationId: getTicketingAttachments @@ -8486,9 +8881,9 @@ paths: - properties: data: $ref: '#/components/schemas/UnifiedAttachmentOutput' - tags: &ref_107 + tags: &ref_112 - ticketing/attachments - security: &ref_108 + security: &ref_113 - JWT: [] post: operationId: addTicketingAttachment @@ -8530,8 +8925,8 @@ paths: application/json: schema: $ref: '#/components/schemas/UnifiedAttachmentOutput' - tags: *ref_107 - security: *ref_108 + tags: *ref_112 + security: *ref_113 /ticketing/attachments/{id}: get: operationId: getTicketingAttachment @@ -8567,8 +8962,8 @@ paths: - properties: data: $ref: '#/components/schemas/UnifiedAttachmentOutput' - tags: *ref_107 - security: *ref_108 + tags: *ref_112 + security: *ref_113 /ticketing/attachments/{id}/download: get: operationId: downloadAttachment @@ -8604,8 +8999,8 @@ paths: - properties: data: $ref: '#/components/schemas/UnifiedAttachmentOutput' - tags: *ref_107 - security: *ref_108 + tags: *ref_112 + security: *ref_113 info: title: Unified Panora API description: The Panora API description @@ -11127,3 +11522,147 @@ components: - shared_link - permission - field_mappings + UnifiedProductOutput: + type: object + properties: + field_mappings: + type: object + properties: {} + id: + type: string + description: The UUID of the department + remote_id: + type: string + description: The remote ID of the department in the context of the 3rd Party + remote_data: + type: object + properties: {} + created_at: + type: object + properties: {} + modified_at: + type: object + properties: {} + required: + - field_mappings + - remote_data + - created_at + - modified_at + UnifiedOrderOutput: + type: object + properties: + name: + type: string + description: The name of the order + field_mappings: + type: object + properties: {} + id: + type: string + description: The UUID of the order + remote_id: + type: string + description: The remote ID of the order in the context of the 3rd Party + remote_data: + type: object + properties: {} + created_at: + type: object + properties: {} + modified_at: + type: object + properties: {} + required: + - field_mappings + - remote_data + - created_at + - modified_at + UnifiedCustomerOutput: + type: object + properties: + name: + type: string + description: The name of the customer + field_mappings: + type: object + properties: {} + id: + type: string + description: The UUID of the customer + remote_id: + type: string + description: The remote ID of the customer in the context of the 3rd Party + remote_data: + type: object + properties: {} + created_at: + type: object + properties: {} + modified_at: + type: object + properties: {} + required: + - field_mappings + - remote_data + - created_at + - modified_at + UnifiedFulfillmentOutput: + type: object + properties: + name: + type: string + description: The name of the fulfillment + field_mappings: + type: object + properties: {} + id: + type: string + description: The UUID of the fulfillment + remote_id: + type: string + description: The remote ID of the fulfillment in the context of the 3rd Party + remote_data: + type: object + properties: {} + created_at: + type: object + properties: {} + modified_at: + type: object + properties: {} + required: + - field_mappings + - remote_data + - created_at + - modified_at + UnifiedFulfillmentOrdersOutput: + type: object + properties: + name: + type: string + description: The name of the fulfillmentorders + field_mappings: + type: object + properties: {} + id: + type: string + description: The UUID of the fulfillmentorders + remote_id: + type: string + description: >- + The remote ID of the fulfillmentorders in the context of the 3rd + Party + remote_data: + type: object + properties: {} + created_at: + type: object + properties: {} + modified_at: + type: object + properties: {} + required: + - field_mappings + - remote_data + - created_at + - modified_at diff --git a/packages/shared/src/authUrl.ts b/packages/shared/src/authUrl.ts index 51c4f00ea..19d839530 100644 --- a/packages/shared/src/authUrl.ts +++ b/packages/shared/src/authUrl.ts @@ -124,7 +124,7 @@ const handleOAuth2Url = async (input: HandleOAuth2Url) => { if(providerName === 'helpscout') { params = `client_id=${encodeURIComponent(clientId)}&state=${state}`; } - if(providerName === 'pipedrive') { + if(providerName === 'pipedrive' || providerName === 'shopify') { params = `client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodedRedirectUrl}&state=${state}`; } diff --git a/packages/shared/src/connectors/metadata.ts b/packages/shared/src/connectors/metadata.ts index ca94a5521..aa80d7d63 100644 --- a/packages/shared/src/connectors/metadata.ts +++ b/packages/shared/src/connectors/metadata.ts @@ -2772,6 +2772,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { }, 'ecommerce': { 'shopify': { + scopes: 'write_products,read_shipping', urls: { docsUrl: 'https://shopify.dev/docs/apps/build', apiUrl: (store_name: string) => `https://${store_name}.myshopify.com`, @@ -2782,6 +2783,9 @@ export const CONNECTORS_METADATA: ProvidersConfig = { active: false, authStrategy: { strategy: AuthStrategy.oauth2 + }, + options: { + company_subdomain: true, } }, 'magento': { @@ -2795,7 +2799,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { active: false, authStrategy: { strategy: AuthStrategy.oauth2 - } + }, }, 'woocommerce': { urls: { @@ -2808,6 +2812,9 @@ export const CONNECTORS_METADATA: ProvidersConfig = { active: false, authStrategy: { strategy: AuthStrategy.oauth2 + }, + options: { + company_subdomain: true, } }, } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9388635c..4d6ac0563 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -471,6 +471,9 @@ importers: '@sentry/tracing': specifier: ^7.80.0 version: 7.109.0 + '@shopify/shopify-api': + specifier: ^11.1.0 + version: 11.1.0 axios: specifier: ^1.5.1 version: 1.6.8 @@ -5293,6 +5296,43 @@ packages: '@sentry/types': 8.9.2 dev: false + /@shopify/admin-api-client@1.0.0: + resolution: {integrity: sha512-yNLFW+wCmXNnAof6MZKZNPN1CYpFc95UCtQmcqeYc5GLdwvIJVGefUXTPqoBR57jV7b9jF+E6is3mNotEbDlLA==} + dependencies: + '@shopify/graphql-client': 1.0.0 + dev: false + + /@shopify/graphql-client@1.0.0: + resolution: {integrity: sha512-K3F13BK6dB17yJASRE9Oyn7Etpj58R3cOwzFHB06yCLhWLGcI7JllK+HxZQTgtA3wbLonG3yxt5ns6XNKitphw==} + dev: false + + /@shopify/network@3.3.0: + resolution: {integrity: sha512-Lln7vglzLK9KiYhl9ucQFVM7ArlpUM21xkDriBX8kVrqsoBsi+4vFIjf1wjhNPT0J/zHMjky7jiTnxVfdm+xXw==} + engines: {node: '>=18.12.0'} + dev: false + + /@shopify/shopify-api@11.1.0: + resolution: {integrity: sha512-PZpOofefIBKhF/tnms11iaUrNrTNgUcKua6nRGtDXqJbSGVVynV55J8pHUhB/3Z7IDczZgmwA0qdX7X3VZmKiA==} + dependencies: + '@shopify/admin-api-client': 1.0.0 + '@shopify/network': 3.3.0 + '@shopify/storefront-api-client': 1.0.0 + compare-versions: 6.1.1 + isbot: 5.1.13 + jose: 5.6.3 + node-fetch: 2.7.0 + tslib: 2.6.3 + uuid: 10.0.0 + transitivePeerDependencies: + - encoding + dev: false + + /@shopify/storefront-api-client@1.0.0: + resolution: {integrity: sha512-TAyCsD+d/Fiwek3YM1/60pXpQTUUX99DM3HpcIed09By5/rAQbQhYWwf9rZDBIJnGXqS/fl3VO4Tqp+173tocw==} + dependencies: + '@shopify/graphql-client': 1.0.0 + dev: false + /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true @@ -7324,6 +7364,10 @@ packages: repeat-string: 1.6.1 dev: true + /compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + dev: false + /component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} dev: true @@ -9974,6 +10018,11 @@ packages: /isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + /isbot@5.1.13: + resolution: {integrity: sha512-RXtBib4m9zChSb+187EpNQML7Z3u2i34zDdqcRFZnqSJs0xdh91xzJytc5apYVg+9Y4NGnUQ0AIeJvX9FAnCUw==} + engines: {node: '>=18'} + dev: false + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -10533,6 +10582,10 @@ packages: resolution: {integrity: sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==} dev: false + /jose@5.6.3: + resolution: {integrity: sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==} + dev: false + /jotai@2.8.0(@types/react@18.2.75)(react@18.2.0): resolution: {integrity: sha512-yZNMC36FdLOksOr8qga0yLf14miCJlEThlp5DeFJNnqzm2+ZG7wLcJzoOyij5K6U6Xlc5ljQqPDlJRgqW0Y18g==} engines: {node: '>=12.20.0'} @@ -14420,6 +14473,10 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + /tslib@2.6.3: + resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + dev: false + /tslint@5.16.0(typescript@5.4.4): resolution: {integrity: sha512-UxG2yNxJ5pgGwmMzPMYh/CCnCnh0HfPgtlVRDs1ykZklufFBL1ZoTlWFRz2NQjcoEiDoRp+JyT0lhBbbH/obyA==} engines: {node: '>=4.8.0'} @@ -14828,6 +14885,11 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + /uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + dev: false + /uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true