From efa7c5ffc462eff509cbaf8f1f2f11615ff25e2e Mon Sep 17 00:00:00 2001 From: mit <1mitccc@gmail.com> Date: Sat, 31 Aug 2024 08:28:04 +0530 Subject: [PATCH 01/11] Add customer object for webflow-ecommerce-connector --- packages/api/scripts/init.sql | 3 +- packages/api/scripts/seed.sql | 8 +- .../types/original/original.ecommerce.ts | 11 ++- .../src/ecommerce/customer/customer.module.ts | 4 + .../customer/services/webflow/index.ts | 60 ++++++++++++ .../customer/services/webflow/mappers.ts | 91 +++++++++++++++++++ .../customer/services/webflow/types.ts | 26 ++++++ packages/shared/src/connectors/enum.ts | 3 +- packages/shared/src/connectors/index.ts | 2 +- packages/shared/src/connectors/metadata.ts | 2 +- 10 files changed, 200 insertions(+), 10 deletions(-) create mode 100644 packages/api/src/ecommerce/customer/services/webflow/index.ts create mode 100644 packages/api/src/ecommerce/customer/services/webflow/mappers.ts create mode 100644 packages/api/src/ecommerce/customer/services/webflow/types.ts diff --git a/packages/api/scripts/init.sql b/packages/api/scripts/init.sql index 71a8c80ec..d90bf6bbc 100644 --- a/packages/api/scripts/init.sql +++ b/packages/api/scripts/init.sql @@ -549,7 +549,8 @@ CREATE TABLE connector_sets ecom_amazon boolean NULL, ecom_squarespace boolean NULL, ats_ashby boolean NULL, - CONSTRAINT PK_project_connector PRIMARY KEY ( id_connector_set ) + ecom_webflow boolean NULL, +CONSTRAINT PK_project_connector PRIMARY KEY ( id_connector_set ) ); diff --git a/packages/api/scripts/seed.sql b/packages/api/scripts/seed.sql index 233841fa2..124e4b592 100644 --- a/packages/api/scripts/seed.sql +++ b/packages/api/scripts/seed.sql @@ -1,10 +1,10 @@ INSERT INTO users (id_user, identification_strategy, email, password_hash, first_name, last_name) VALUES ('0ce39030-2901-4c56-8db0-5e326182ec6b', 'b2c','local@panora.dev', '$2b$10$Y7Q8TWGyGuc5ecdIASbBsuXMo3q/Rs3/cnY.mLZP4tUgfGUOCUBlG', 'local', 'Panora'); -INSERT INTO connector_sets (id_connector_set, crm_hubspot, crm_zoho, crm_pipedrive, crm_attio, crm_zendesk, crm_close, tcg_zendesk, tcg_gorgias, tcg_front, tcg_jira, tcg_gitlab, fs_box, tcg_github, hris_deel, hris_sage, ats_ashby) VALUES - ('1709da40-17f7-4d3a-93a0-96dc5da6ddd7', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE), - ('852dfff8-ab63-4530-ae49-e4b2924407f8', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE), - ('aed0f856-f802-4a79-8640-66d441581a99', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE); +INSERT INTO connector_sets (id_connector_set, crm_hubspot, crm_zoho, crm_pipedrive, crm_attio, crm_zendesk, crm_close, tcg_zendesk, tcg_gorgias, tcg_front, tcg_jira, tcg_gitlab, fs_box, tcg_github, hris_deel, hris_sage, ats_ashby, ecom_webflow) VALUES + ('1709da40-17f7-4d3a-93a0-96dc5da6ddd7', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE), + ('852dfff8-ab63-4530-ae49-e4b2924407f8', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE), + ('aed0f856-f802-4a79-8640-66d441581a99', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE); INSERT INTO projects (id_project, name, sync_mode, id_user, id_connector_set) VALUES ('1e468c15-aa57-4448-aa2b-7fed640d1e3d', 'Project 1', 'pull', '0ce39030-2901-4c56-8db0-5e326182ec6b', '1709da40-17f7-4d3a-93a0-96dc5da6ddd7'), 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 95589729e..d7d034a72 100644 --- a/packages/api/src/@core/utils/types/original/original.ecommerce.ts +++ b/packages/api/src/@core/utils/types/original/original.ecommerce.ts @@ -1,3 +1,8 @@ +import { + WebflowCustomerInput, + WebflowCustomerOutput, +} from '@ecommerce/customer/services/webflow/types'; + /* INPUT */ import { AmazonCustomerOutput } from '@ecommerce/customer/services/amazon/types'; @@ -70,7 +75,8 @@ export type OriginalFulfillmentOrdersInput = ShopifyFulfillmentOrdersInput; export type OriginalCustomerInput = | ShopifyCustomerInput | WoocommerceCustomerInput - | SquarespaceCustomerInput; + | SquarespaceCustomerInput + | WebflowCustomerInput; /* fulfillment */ export type OriginalFulfillmentInput = ShopifyFulfillmentInput; @@ -105,7 +111,8 @@ export type OriginalCustomerOutput = | ShopifyCustomerOutput | WoocommerceCustomerOutput | SquarespaceCustomerOutput - | AmazonCustomerOutput; + | AmazonCustomerOutput + | WebflowCustomerOutput; /* fulfillment */ export type OriginalFulfillmentOutput = ShopifyFulfillmentOutput; diff --git a/packages/api/src/ecommerce/customer/customer.module.ts b/packages/api/src/ecommerce/customer/customer.module.ts index a09f9f380..00b95c5f9 100644 --- a/packages/api/src/ecommerce/customer/customer.module.ts +++ b/packages/api/src/ecommerce/customer/customer.module.ts @@ -1,3 +1,5 @@ +import { WebflowCustomerMapper } from './services/webflow/mappers'; +import { WebflowService } from './services/webflow'; 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'; @@ -30,6 +32,8 @@ import { AmazonCustomerMapper } from './services/amazon/mappers'; /* PROVIDERS SERVICES */ ShopifyService, WoocommerceService, + WebflowService, + WebflowCustomerMapper, ], exports: [SyncService], }) diff --git a/packages/api/src/ecommerce/customer/services/webflow/index.ts b/packages/api/src/ecommerce/customer/services/webflow/index.ts new file mode 100644 index 000000000..1256429a8 --- /dev/null +++ b/packages/api/src/ecommerce/customer/services/webflow/index.ts @@ -0,0 +1,60 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { ICustomerService } from '@ecommerce/customer/types'; +import { Injectable } from '@nestjs/common'; +import { EcommerceObject } from '@panora/shared'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { WebflowCustomerOutput } from './types'; + +@Injectable() +export class WebflowService implements ICustomerService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + EcommerceObject.customer.toUpperCase() + ':' + WebflowService.name, + ); + this.registry.registerService('webflow', this); + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'webflow', + vertical: 'ecommerce', + }, + }); + + const resp = await axios.get(`${connection.account_url}/users`, { + headers: { + 'Content-Type': 'application/json', + authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + const customers: WebflowCustomerOutput[] = resp.data.users; + + this.logger.log(`Synced webflow customers !`); + + return { + data: customers, + message: 'Webflow customers retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/ecommerce/customer/services/webflow/mappers.ts b/packages/api/src/ecommerce/customer/services/webflow/mappers.ts new file mode 100644 index 000000000..7aea5717d --- /dev/null +++ b/packages/api/src/ecommerce/customer/services/webflow/mappers.ts @@ -0,0 +1,91 @@ +import { WebflowCustomerInput, WebflowCustomerOutput } from './types'; +import { + UnifiedEcommerceCustomerInput, + UnifiedEcommerceCustomerOutput, +} from '@ecommerce/customer/types/model.unified'; +import { ICustomerMapper } from '@ecommerce/customer/types'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { Injectable } from '@nestjs/common'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { Utils } from '@ecommerce/@lib/@utils'; + +@Injectable() +export class WebflowCustomerMapper implements ICustomerMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService( + 'ecommerce', + 'customer', + 'webflow', + this, + ); + } + + async desunify( + source: UnifiedEcommerceCustomerInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + return; + } + + async unify( + source: WebflowCustomerOutput | WebflowCustomerOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise< + UnifiedEcommerceCustomerOutput | UnifiedEcommerceCustomerOutput[] + > { + if (!Array.isArray(source)) { + return await this.mapSingleCustomerToUnified( + source, + connectionId, + customFieldMappings, + ); + } + // Handling array of WebflowCustomerOutput + return Promise.all( + source.map((customer) => + this.mapSingleCustomerToUnified( + customer, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleCustomerToUnified( + customer: WebflowCustomerOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result = { + remote_id: customer.id, + remote_data: customer, + email: customer.data.email, + first_name: customer.data.name, + last_name: null, + phone_number: null, + addresses: [], + field_mappings: + customFieldMappings?.reduce((acc, mapping) => { + acc[mapping.slug] = customer[mapping.remote_id]; + return acc; + }, {} as Record) || {}, + }; + + return result; + } +} diff --git a/packages/api/src/ecommerce/customer/services/webflow/types.ts b/packages/api/src/ecommerce/customer/services/webflow/types.ts new file mode 100644 index 000000000..a55c2da4f --- /dev/null +++ b/packages/api/src/ecommerce/customer/services/webflow/types.ts @@ -0,0 +1,26 @@ +export interface WebflowCustomerInput { + id: string; + isEmailVerified: boolean; + lastUpdated: string; + invitedOn: string; + createdOn: string; + lastLogin: string; + status: string; + accessGroups: AccessGroup[]; + data: UserData; +} + +type AccessGroup = { + slug: string; + type: string; +}; + +type UserData = { + name: string; + email: string; + 'accept-privacy': boolean; + 'accept-communications': boolean; + [key: string]: any; +}; + +export type WebflowCustomerOutput = Partial; diff --git a/packages/shared/src/connectors/enum.ts b/packages/shared/src/connectors/enum.ts index a66547391..6cea8afa5 100644 --- a/packages/shared/src/connectors/enum.ts +++ b/packages/shared/src/connectors/enum.ts @@ -11,7 +11,8 @@ export enum EcommerceConnectors { SHOPIFY = 'shopify', WOOCOMMERCE = 'woocommerce', SQUARESPACE = 'squarespace', - AMAZON = 'amazon' + AMAZON = 'amazon', + WEBFLOW = 'webflow' } export enum TicketingConnectors { diff --git a/packages/shared/src/connectors/index.ts b/packages/shared/src/connectors/index.ts index 105ca557a..27c8b40ec 100644 --- a/packages/shared/src/connectors/index.ts +++ b/packages/shared/src/connectors/index.ts @@ -5,4 +5,4 @@ export const ACCOUNTING_PROVIDERS = []; export const TICKETING_PROVIDERS = ['zendesk', 'front', 'jira', 'gitlab', 'github']; export const MARKETINGAUTOMATION_PROVIDERS = []; export const FILESTORAGE_PROVIDERS = ['box']; -export const ECOMMERCE_PROVIDERS = ['shopify', 'woocommerce', 'squarespace', 'amazon']; +export const ECOMMERCE_PROVIDERS = ['shopify', 'woocommerce', 'squarespace', 'amazon', 'webflow']; diff --git a/packages/shared/src/connectors/metadata.ts b/packages/shared/src/connectors/metadata.ts index b5baeafac..3949a4f6c 100644 --- a/packages/shared/src/connectors/metadata.ts +++ b/packages/shared/src/connectors/metadata.ts @@ -2920,7 +2920,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { }, logoPath: 'https://dailybrand.co.zw/wp-content/uploads/2023/10/webflow-2.png', description: 'Sync & Create orders, fulfillments, fulfillment orders, customers and products', - active: false, + active: true, authStrategy: { strategy: AuthStrategy.oauth2 }, From 6d94b36e3f4624eb5bc240c934ee3f3e4cac40ab Mon Sep 17 00:00:00 2001 From: mit <1mitccc@gmail.com> Date: Sat, 31 Aug 2024 17:13:14 +0530 Subject: [PATCH 02/11] Add types.ts for product object of webflow-ecommerce service --- .../product/services/webflow/index.ts | 0 .../product/services/webflow/mappers.ts | 0 .../product/services/webflow/types.ts | 83 +++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 packages/api/src/ecommerce/product/services/webflow/index.ts create mode 100644 packages/api/src/ecommerce/product/services/webflow/mappers.ts create mode 100644 packages/api/src/ecommerce/product/services/webflow/types.ts diff --git a/packages/api/src/ecommerce/product/services/webflow/index.ts b/packages/api/src/ecommerce/product/services/webflow/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/api/src/ecommerce/product/services/webflow/mappers.ts b/packages/api/src/ecommerce/product/services/webflow/mappers.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/api/src/ecommerce/product/services/webflow/types.ts b/packages/api/src/ecommerce/product/services/webflow/types.ts new file mode 100644 index 000000000..46c6976a2 --- /dev/null +++ b/packages/api/src/ecommerce/product/services/webflow/types.ts @@ -0,0 +1,83 @@ +// reference: https://docs.developers.webflow.com/data/reference/list-products + +export interface WebflowProductInput { + product: ProductData; + skus: SkuData[]; + publishStatus: 'staging' | 'live'; +} + +export interface ProductData { + id: string; + cmsLocaleId: string; + lastPublished: string; + lastUpdated: string; + createdOn: string; + isArchived: boolean; + isDraft: boolean; + fieldData: ProductFieldData; +} + +export interface ProductFieldData { + name: string; // required + slug: string; // required + description?: string; + shippable: boolean; + skuProperties?: SkuProperty[]; + categories?: string[]; + taxCategory?: string; + defaultSku: string; + ecProductType?: string; + [key: string]: any; // for custom fields +} + +export interface SkuProperty { + id: string; // required + name: string; // required + enum: VariantOption[]; +} + +export interface VariantOption { + id: string; // required + name: string; // required + slug: string; // required +} + +export interface SkuData { + id: string; + cmsLocaleId: string; + lastPublished: string; + lastUpdated: string; + createdOn: string; + fieldData: SkuFieldData; +} + +export interface SkuFieldData { + skuValues: { [key: string]: string }; // maps SKU property ID to SKU value ID + name: string; // required + slug: string; // required + price: Price; // required + compareAtPrice?: Price; + ecSkuBillingMethod?: 'one-time' | 'subscription'; + ecSkuSubscriptionPlan?: SubscriptionPlan; + trackInventory?: boolean; // Defaults to false + quantity?: number; + [key: string]: any; // for custom +} + +export interface Price { + value: number; // required + unit: string; // required +} + +export interface SubscriptionPlan { + interval: 'day' | 'week' | 'month' | 'year'; // required + frequency: number; // required + trial?: number; + plans?: { + id: string; + platform: string; + status: string; + }[]; +} + +export type WebflowProductOutput = Partial; From f91d79dd2a17e45707c7c235300f009973416a90 Mon Sep 17 00:00:00 2001 From: mit <1mitccc@gmail.com> Date: Sat, 31 Aug 2024 18:38:32 +0530 Subject: [PATCH 03/11] Add mappers.ts for product object of webflow-ecommerce service --- .../product/services/webflow/mappers.ts | 119 ++++++++++++++++++ .../product/services/webflow/types.ts | 49 +++++--- 2 files changed, 154 insertions(+), 14 deletions(-) diff --git a/packages/api/src/ecommerce/product/services/webflow/mappers.ts b/packages/api/src/ecommerce/product/services/webflow/mappers.ts index e69de29bb..be7e90b6d 100644 --- a/packages/api/src/ecommerce/product/services/webflow/mappers.ts +++ b/packages/api/src/ecommerce/product/services/webflow/mappers.ts @@ -0,0 +1,119 @@ +import { + UnifiedEcommerceProductInput, + UnifiedEcommerceProductOutput, +} from '@ecommerce/product/types/model.unified'; +import { IProductMapper } from '@ecommerce/product/types'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { Injectable } from '@nestjs/common'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { Utils } from '@ecommerce/@lib/@utils'; +import { WebflowProductInput, WebflowProductOutput } from './types'; + +@Injectable() +export class WebflowProductMapper implements IProductMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService( + 'ecommerce', + 'product', + 'webflow', + this, + ); + } + + async desunify( + source: UnifiedEcommerceProductInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise> { + const res: Partial = { + product: { + fieldData: { + name: source.variants?.[0]?.title, + slug: source.product_url?.split('/').pop() || '', + description: source.description, + shippable: true, + skuProperties: [], + }, + }, + skus: source.variants?.map((item) => ({ + fieldData: { + name: item.title, + slug: item.sku, + sku: item.sku, + price: { + value: item.price, + unit: 'USD', + }, + }, + })), + }; + + customFieldMappings?.forEach((mapping) => { + if (mapping.slug === 'publishStatus') { + res.publishStatus = source[mapping.remote_id]; + return; + } + + res.product.fieldData[mapping.slug] = source[mapping.remote_id]; + }); + + return res; + } + + async unify( + source: WebflowProductOutput | WebflowProductOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return await this.mapSingleProductToUnified( + source, + connectionId, + customFieldMappings, + ); + } + // Handling array of WebflowProductOutput + return Promise.all( + source.map((data) => + this.mapSingleProductToUnified(data, connectionId, customFieldMappings), + ), + ); + } + + private async mapSingleProductToUnified( + data: WebflowProductOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + return { + remote_id: data.product.id, + remote_data: data, + images_urls: + data.skus?.map((sku) => sku?.fieldData?.mainImage?.url) || [], + description: data.product.fieldData?.description, + tags: data.product.fieldData?.categories, + created_at: data.product?.createdOn, + modified_at: data.product?.lastUpdated, + variants: data.skus?.map((sku) => ({ + title: sku?.fieldData?.name, + price: sku?.fieldData?.price?.value, + sku: sku?.fieldData?.sku, + inventory_quantity: sku?.fieldData?.quantity, + weight: sku?.fieldData?.weight, + options: null, + })), + }; + } +} diff --git a/packages/api/src/ecommerce/product/services/webflow/types.ts b/packages/api/src/ecommerce/product/services/webflow/types.ts index 46c6976a2..e6d9ada21 100644 --- a/packages/api/src/ecommerce/product/services/webflow/types.ts +++ b/packages/api/src/ecommerce/product/services/webflow/types.ts @@ -7,13 +7,13 @@ export interface WebflowProductInput { } export interface ProductData { - id: string; - cmsLocaleId: string; - lastPublished: string; - lastUpdated: string; - createdOn: string; - isArchived: boolean; - isDraft: boolean; + id?: string; + cmsLocaleId?: string; + lastPublished?: string; + lastUpdated?: string; + createdOn?: string; + isArchived?: boolean; + isDraft?: boolean; fieldData: ProductFieldData; } @@ -25,7 +25,7 @@ export interface ProductFieldData { skuProperties?: SkuProperty[]; categories?: string[]; taxCategory?: string; - defaultSku: string; + defaultSku?: string; ecProductType?: string; [key: string]: any; // for custom fields } @@ -43,19 +43,28 @@ export interface VariantOption { } export interface SkuData { - id: string; - cmsLocaleId: string; - lastPublished: string; - lastUpdated: string; - createdOn: string; + id?: string; + cmsLocaleId?: string; + lastPublished?: string; + lastUpdated?: string; + createdOn?: string; fieldData: SkuFieldData; } export interface SkuFieldData { - skuValues: { [key: string]: string }; // maps SKU property ID to SKU value ID + skuValues?: { [key: string]: string }; // maps SKU property ID to SKU value ID name: string; // required slug: string; // required price: Price; // required + product?: string; + width?: number; + length?: number; + height?: number; + weight?: number; + sku: string; + mainImage?: Image | null; + moreImages?: Image[]; + downloadFiles?: DownloadFile[]; compareAtPrice?: Price; ecSkuBillingMethod?: 'one-time' | 'subscription'; ecSkuSubscriptionPlan?: SubscriptionPlan; @@ -80,4 +89,16 @@ export interface SubscriptionPlan { }[]; } +export interface Image { + fileId: string; + url: string; + alt?: string | null; +} + +export interface DownloadFile { + name: string; + url: string; + id: string; +} + export type WebflowProductOutput = Partial; From 07a88df5f13f167f58ba73f92c760c4d18dd3248 Mon Sep 17 00:00:00 2001 From: mit <1mitccc@gmail.com> Date: Sat, 31 Aug 2024 19:49:51 +0530 Subject: [PATCH 04/11] Add index.ts for product object of webflow-ecommerce service --- .../types/original/original.ecommerce.ts | 6 +- .../src/ecommerce/product/product.module.ts | 4 + .../product/services/webflow/index.ts | 99 +++++++++++++++++++ .../product/services/webflow/mappers.ts | 17 ++-- .../product/services/webflow/types.ts | 9 +- 5 files changed, 122 insertions(+), 13 deletions(-) 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 d7d034a72..4c262cba5 100644 --- a/packages/api/src/@core/utils/types/original/original.ecommerce.ts +++ b/packages/api/src/@core/utils/types/original/original.ecommerce.ts @@ -1,3 +1,5 @@ +import { WebflowProductInput, WebflowProductOutput } from '@ecommerce/product/services/webflow/types'; + import { WebflowCustomerInput, WebflowCustomerOutput, @@ -59,7 +61,7 @@ import { export type OriginalProductInput = | ShopifyProductInput | WoocommerceProductInput - | SquarespaceProductInput; + | SquarespaceProductInput | WebflowProductInput; /* order */ export type OriginalOrderInput = @@ -94,7 +96,7 @@ export type EcommerceObjectInput = export type OriginalProductOutput = | ShopifyProductOutput | WoocommerceProductOutput - | SquarespaceProductOutput; + | SquarespaceProductOutput | WebflowProductOutput; /* order */ export type OriginalOrderOutput = diff --git a/packages/api/src/ecommerce/product/product.module.ts b/packages/api/src/ecommerce/product/product.module.ts index feb69f6d7..ac43271e3 100644 --- a/packages/api/src/ecommerce/product/product.module.ts +++ b/packages/api/src/ecommerce/product/product.module.ts @@ -1,3 +1,5 @@ +import { WebflowProductMapper } from './services/webflow/mappers'; +import { WebflowService } from './services/webflow'; 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'; @@ -31,6 +33,8 @@ import { SyncService } from './sync/sync.service'; ShopifyService, WoocommerceService, SquarespaceService, + WebflowService, + WebflowProductMapper, ], exports: [SyncService], }) diff --git a/packages/api/src/ecommerce/product/services/webflow/index.ts b/packages/api/src/ecommerce/product/services/webflow/index.ts index e69de29bb..5636377e7 100644 --- a/packages/api/src/ecommerce/product/services/webflow/index.ts +++ b/packages/api/src/ecommerce/product/services/webflow/index.ts @@ -0,0 +1,99 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { IProductService } from '@ecommerce/product/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { EcommerceObject } from '@panora/shared'; +import { WebflowProductInput, WebflowProductOutput } from './types'; +import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; +import { OriginalProductOutput } from '@@core/utils/types/original/original.ecommerce'; + +@Injectable() +export class WebflowService implements IProductService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + EcommerceObject.product.toUpperCase() + ':' + WebflowService.name, + ); + this.registry.registerService('webflow', this); + } + + async addProduct( + productData: WebflowProductInput, + linkedUserId: string, + ): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'webflow', + vertical: 'ecommerce', + }, + }); + const resp = await axios.post( + // https://api.webflow.com/v2/sites/{site_id}/products + `${connection.account_url}/products`, + productData, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + return { + data: resp.data, + message: 'Webflow product created', + statusCode: 201, + }; + } catch (error) { + throw error; + } + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'webflow', + vertical: 'ecommerce', + }, + }); + const resp = await axios.get( + // https://api.webflow.com/v2/sites/{site_id}/products + `${connection.account_url}/products`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + const products: WebflowProductOutput[] = resp.data.items; + this.logger.log(`Synced webflow products !`); + + return { + data: products, + message: 'Webflow products retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/ecommerce/product/services/webflow/mappers.ts b/packages/api/src/ecommerce/product/services/webflow/mappers.ts index be7e90b6d..1a3bc479c 100644 --- a/packages/api/src/ecommerce/product/services/webflow/mappers.ts +++ b/packages/api/src/ecommerce/product/services/webflow/mappers.ts @@ -38,20 +38,19 @@ export class WebflowProductMapper implements IProductMapper { slug: source.product_url?.split('/').pop() || '', description: source.description, shippable: true, - skuProperties: [], }, }, - skus: source.variants?.map((item) => ({ + sku: { fieldData: { - name: item.title, - slug: item.sku, - sku: item.sku, + name: source.variants?.[0]?.title, + slug: source.variants?.[0]?.sku, + sku: source.variants?.[0]?.sku, price: { - value: item.price, + value: parseInt(source.variants?.[0]?.price.toString(), 10), unit: 'USD', }, }, - })), + }, }; customFieldMappings?.forEach((mapping) => { @@ -101,7 +100,9 @@ export class WebflowProductMapper implements IProductMapper { remote_id: data.product.id, remote_data: data, images_urls: - data.skus?.map((sku) => sku?.fieldData?.mainImage?.url) || [], + data.skus + ?.map((sku) => sku?.fieldData?.mainImage?.url) + .filter((url) => Boolean(url)) || [], description: data.product.fieldData?.description, tags: data.product.fieldData?.categories, created_at: data.product?.createdOn, diff --git a/packages/api/src/ecommerce/product/services/webflow/types.ts b/packages/api/src/ecommerce/product/services/webflow/types.ts index e6d9ada21..773d7ac87 100644 --- a/packages/api/src/ecommerce/product/services/webflow/types.ts +++ b/packages/api/src/ecommerce/product/services/webflow/types.ts @@ -1,9 +1,8 @@ // reference: https://docs.developers.webflow.com/data/reference/list-products -export interface WebflowProductInput { +export interface WebflowProductOutput { product: ProductData; skus: SkuData[]; - publishStatus: 'staging' | 'live'; } export interface ProductData { @@ -101,4 +100,8 @@ export interface DownloadFile { id: string; } -export type WebflowProductOutput = Partial; +export interface WebflowProductInput { + product: ProductData; + sku: SkuData; + publishStatus: 'staging' | 'live'; +} From 220931dddca667be8e2ab838eec58a20b2bdd5e1 Mon Sep 17 00:00:00 2001 From: mit <1mitccc@gmail.com> Date: Sun, 1 Sep 2024 10:38:21 +0530 Subject: [PATCH 05/11] Add types.ts for order object of webflow connector --- .../api/src/ecommerce/order/webflow/types.ts | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 packages/api/src/ecommerce/order/webflow/types.ts diff --git a/packages/api/src/ecommerce/order/webflow/types.ts b/packages/api/src/ecommerce/order/webflow/types.ts new file mode 100644 index 000000000..5b3f5bacc --- /dev/null +++ b/packages/api/src/ecommerce/order/webflow/types.ts @@ -0,0 +1,173 @@ +export interface WebflowOrderOutput { + orderId: string; + status: + | 'pending' + | 'unfulfilled' + | 'fulfilled' + | 'disputed' + | 'dispute-lost' + | 'refunded'; + comment?: string | null; // A comment string for this Order, which is editable by API user (not used by Webflow). + orderComment?: string | null; // A comment that the customer left when making their Order + acceptedOn: string | null; + fulfilledOn: string | null; + refundedOn: string | null; + disputedOn: string | null; + disputeUpdatedOn: string | null; + disputeLastStatus: DisputeStatus | null; + customerPaid: Money; + netAmount: Money; + applicationFee: Money; + allAddresses: Address[]; + shippingAddress: Address; + shippingProvider?: string | null; + shippingTracking?: string | null; + shippingTrackingURL: string | null; + customerInfo: { + fullName: string; + email: string; + }; + purchasedItems: PurchasedItem[]; + purchasedItemsCount: number; + stripeDetails: StripeDetails | null; + stripeCard: StripeCard | null; + paypalDetails: PaypalDetails | null; + paymentProcessor?: string; + customData: Array>; // Array of objects + metadata: { + isBuyNow: boolean; + hasDownloads?: boolean; + paymentProcessor?: string; + }; + billingAddress: Address; + totals?: Totals; + isCustomerDeleted?: boolean; + isShippingRequired: boolean; + hasDownloads?: boolean; + downloadFiles?: { + id: string; + name: string; + url: string; + }[]; +} + +interface Money { + unit: string; // The three-letter ISO currency code + value: number; + string: string; +} + +interface Address { + type: 'billing' | 'shipping'; + japanType?: 'kana' | 'kanji' | null; // Represents a Japan-only address format. This field will only appear on orders placed from Japan. + addressee: string; + line1: string; + line2?: string; + city: string; + state: string; + country: string; + postalCode: string; +} + +interface FileObjectVariant { + url: string; + originalFileName: string; + size: number; + width: number; // in pixels + height: number; // in pixels +} + +interface FileObject { + size: number; + originalFileName: string; + createdOn: string; + contentType: string; // The MIME type of the image + width: number; // The image width in pixels + height: number; // The image height in pixels + variants: FileObjectVariant[]; +} + +interface PurchasedItem { + count: number; + rowTotal: Money; + productId: string; + productName: string; + productSlug: string; + variantId: string; + variantName: string; + variantSlug: string; + variantSKU: string; + variantImage: { + url: string; + file: FileObject | null; + }; + variantPrice: Money; + weight: number | null; + height: number | null; + width: number | null; + length: number | null; +} + +interface StripeDetails { + customerId: string | null; + paymentMethod: string | null; + chargeId: string | null; + disputeId: string | null; + paymentIntentId: string | null; + subscriptionId: string | null; + refundId: string | null; + refundReason: string | null; +} + +interface PaypalDetails { + orderId: string; + payerId: string; + captureId: string; + refundId: string; + refundReason: string; + disputeId: string; +} + +interface StripeCard { + last4: string; + brand: StripeCardBrands; + ownerName: string; + expires: { + month: number; + year: number; + }; +} + +interface Totals { + subtotal: Money; + extras: { + type: 'tax' | 'shipping' | 'discount' | 'discount-shipping'; + name: string; + description?: string; + price: Money; + }[]; + total: Money; +} + +enum DisputeStatus { + WARNING_NEEDS_RESPONSE = 'warning_needs_response', + WARNING_UNDER_REVIEW = 'warning_under_review', + WARNING_CLOSED = 'warning_closed', + NEEDS_RESPONSE = 'needs_response', + UNDER_REVIEW = 'under_review', + CHARGE_REFUNDED = 'charge_refunded', + WON = 'won', + LOST = 'lost', +} + +enum StripeCardBrands { + VISA = 'Visa', + AMEX = 'American Express', + MASTERCARD = 'MasterCard', + DISCOVER = 'Discover', + JCB = 'JCB', + DINERS_CLUB = 'Diners Club', + UNKNOWN = 'Unknown', +} + +export type WebflowOrderInput = Partial; From ec3aa328f77b9f1a3c2d770b04e0bdffa2a1fa93 Mon Sep 17 00:00:00 2001 From: mit <1mitccc@gmail.com> Date: Sun, 1 Sep 2024 10:42:37 +0530 Subject: [PATCH 06/11] Fix dir path --- packages/api/src/ecommerce/order/services/webflow/index.ts | 0 packages/api/src/ecommerce/order/services/webflow/mappers.ts | 0 packages/api/src/ecommerce/order/{ => services}/webflow/types.ts | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/api/src/ecommerce/order/services/webflow/index.ts create mode 100644 packages/api/src/ecommerce/order/services/webflow/mappers.ts rename packages/api/src/ecommerce/order/{ => services}/webflow/types.ts (100%) diff --git a/packages/api/src/ecommerce/order/services/webflow/index.ts b/packages/api/src/ecommerce/order/services/webflow/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/api/src/ecommerce/order/services/webflow/mappers.ts b/packages/api/src/ecommerce/order/services/webflow/mappers.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/api/src/ecommerce/order/webflow/types.ts b/packages/api/src/ecommerce/order/services/webflow/types.ts similarity index 100% rename from packages/api/src/ecommerce/order/webflow/types.ts rename to packages/api/src/ecommerce/order/services/webflow/types.ts From d9bee09a16e4131fda201eab85801b6fc866a001 Mon Sep 17 00:00:00 2001 From: mit <1mitccc@gmail.com> Date: Sun, 1 Sep 2024 11:52:42 +0530 Subject: [PATCH 07/11] Add mappers.ts for order object of webflow connector --- .../order/services/webflow/mappers.ts | 126 ++++++++++++++++++ .../ecommerce/order/services/webflow/types.ts | 6 +- 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/packages/api/src/ecommerce/order/services/webflow/mappers.ts b/packages/api/src/ecommerce/order/services/webflow/mappers.ts index e69de29bb..40400168a 100644 --- a/packages/api/src/ecommerce/order/services/webflow/mappers.ts +++ b/packages/api/src/ecommerce/order/services/webflow/mappers.ts @@ -0,0 +1,126 @@ +import { + UnifiedEcommerceOrderInput, + UnifiedEcommerceOrderOutput, +} from '@ecommerce/order/types/model.unified'; +import { IOrderMapper } from '@ecommerce/order/types'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { Injectable } from '@nestjs/common'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { Utils } from '@ecommerce/@lib/@utils'; +import { WebflowOrderInput, WebflowOrderOutput } from './types'; + +@Injectable() +export class WebflowOrderMapper implements IOrderMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService('ecommerce', 'order', 'webflow', this); + } + + async desunify( + source: UnifiedEcommerceOrderInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + return; + } + + async unify( + source: WebflowOrderOutput | WebflowOrderOutput[], + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + if (!Array.isArray(source)) { + return await this.mapSingleOrderToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((order) => + this.mapSingleOrderToUnified(order, connectionId, customFieldMappings), + ), + ); + } + + private async mapSingleOrderToUnified( + source: WebflowOrderOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: Partial = { + remote_id: source.orderId, + remote_data: source, + created_at: source.acceptedOn, + order_status: this.mapWebflowStatusToUnified(source.status), + currency: source.customerPaid.unit, + total_price: source.totals.total.value, + fulfillment_status: this.mapWebflowStatusToUnified(source.status), + items: + source.purchasedItems?.map((item) => ({ + product_id: item.productId, + variant_id: item.variantId, + sku: item.variantSKU, + title: item.productName, + quantity: item.count, + price: item.variantPrice.value.toString(), + total: item.rowTotal.value.toString(), + variant_title: item.variantName, + weight: item.weight, + properties: [ + { + name: 'image_url', + value: item.variantImage.url, + }, + ], + })) || [], + field_mappings: {}, + }; + + result.total_discount = source.totals.extras + .filter((extra) => ['discount', 'discount-shipping'].includes(extra.type)) + .reduce((acc, curr) => acc + curr.price.value, 0); + + result.total_shipping = source.totals.extras + .filter((extra) => extra.type === 'shipping') + .reduce((acc, curr) => acc + curr.price.value, 0); + + result.total_tax = source.totals.extras + .filter((extra) => extra.type === 'tax') + .reduce((acc, curr) => acc + curr.price.value, 0); + + if (customFieldMappings && source.customData) { + for (const [key, value] of Object.entries(source.customData)) { + if (customFieldMappings.find((m) => m.slug === key)) { + result.field_mappings[key] = value; + } + } + } + + return result; + } + + private mapWebflowStatusToUnified( + status?: WebflowOrderInput['status'], + ): string { + switch (status) { + case 'pending': + return 'PENDING'; + case 'fulfilled': + return 'FULLFILLED'; + default: + return status; + } + } +} diff --git a/packages/api/src/ecommerce/order/services/webflow/types.ts b/packages/api/src/ecommerce/order/services/webflow/types.ts index 5b3f5bacc..f6deb88ab 100644 --- a/packages/api/src/ecommerce/order/services/webflow/types.ts +++ b/packages/api/src/ecommerce/order/services/webflow/types.ts @@ -1,3 +1,5 @@ +import { CurrencyCode } from '@@core/utils/types'; + export interface WebflowOrderOutput { orderId: string; status: @@ -52,9 +54,9 @@ export interface WebflowOrderOutput { } interface Money { - unit: string; // The three-letter ISO currency code + unit: CurrencyCode; // The three-letter ISO currency code value: number; - string: string; + string: string; // example: "$ 109.05 USD" } interface Address { From a3501d1a1a47827fecd50b804e5350ffae998a48 Mon Sep 17 00:00:00 2001 From: mit <1mitccc@gmail.com> Date: Sun, 1 Sep 2024 12:08:51 +0530 Subject: [PATCH 08/11] Add index.ts for order object of webflow connector --- .../ecommerce/order/services/webflow/index.ts | 58 +++++++++++++++++++ .../ecommerce/order/services/webflow/types.ts | 6 +- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/packages/api/src/ecommerce/order/services/webflow/index.ts b/packages/api/src/ecommerce/order/services/webflow/index.ts index e69de29bb..964fe2bc2 100644 --- a/packages/api/src/ecommerce/order/services/webflow/index.ts +++ b/packages/api/src/ecommerce/order/services/webflow/index.ts @@ -0,0 +1,58 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { IOrderService } from '@ecommerce/order/types'; +import { Injectable } from '@nestjs/common'; +import { EcommerceObject } from '@panora/shared'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { WebflowOrderInput, WebflowOrderOutput } from './types'; + +@Injectable() +export class WebflowService implements IOrderService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + EcommerceObject.order.toUpperCase() + ':' + WebflowService.name, + ); + this.registry.registerService('webflow', this); + } + + async sync(data: SyncParam): Promise> { + try { + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: data.linkedUserId, + provider_slug: 'webflow', + vertical: 'ecommerce', + }, + }); + + // ref: https://docs.developers.webflow.com/data/reference/list-orders + const resp = await axios.get(`${connection.account_url}/orders`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + const orders: WebflowOrderOutput[] = resp.data.orders; + this.logger.log(`Synced webflow orders !`); + + return { + data: orders, + message: 'Webflow orders retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/ecommerce/order/services/webflow/types.ts b/packages/api/src/ecommerce/order/services/webflow/types.ts index f6deb88ab..80bde1b47 100644 --- a/packages/api/src/ecommerce/order/services/webflow/types.ts +++ b/packages/api/src/ecommerce/order/services/webflow/types.ts @@ -1,7 +1,7 @@ import { CurrencyCode } from '@@core/utils/types'; -export interface WebflowOrderOutput { - orderId: string; +export interface WebflowOrderInput { + readonly orderId: string; status: | 'pending' | 'unfulfilled' @@ -172,4 +172,4 @@ enum StripeCardBrands { UNKNOWN = 'Unknown', } -export type WebflowOrderInput = Partial; +export type WebflowOrderOutput = Partial; From 4cc21a023f17da9f9cfa35f954c6541eae5ae836 Mon Sep 17 00:00:00 2001 From: mit <1mitccc@gmail.com> Date: Sun, 1 Sep 2024 12:51:23 +0530 Subject: [PATCH 09/11] Cleanup code-loyout in some files --- .../types/original/original.ecommerce.ts | 31 ++++++++++++------- .../src/ecommerce/customer/customer.module.ts | 4 +-- .../api/src/ecommerce/order/order.module.ts | 4 +++ .../src/ecommerce/product/product.module.ts | 4 +-- 4 files changed, 28 insertions(+), 15 deletions(-) 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 4c262cba5..308185fc9 100644 --- a/packages/api/src/@core/utils/types/original/original.ecommerce.ts +++ b/packages/api/src/@core/utils/types/original/original.ecommerce.ts @@ -1,10 +1,3 @@ -import { WebflowProductInput, WebflowProductOutput } from '@ecommerce/product/services/webflow/types'; - -import { - WebflowCustomerInput, - WebflowCustomerOutput, -} from '@ecommerce/customer/services/webflow/types'; - /* INPUT */ import { AmazonCustomerOutput } from '@ecommerce/customer/services/amazon/types'; @@ -56,19 +49,33 @@ import { WoocommerceProductInput, WoocommerceProductOutput, } from '@ecommerce/product/services/woocommerce/types'; +import { + WebflowOrderInput, + WebflowOrderOutput, +} from '@ecommerce/order/services/webflow/types'; +import { + WebflowProductInput, + WebflowProductOutput, +} from '@ecommerce/product/services/webflow/types'; +import { + WebflowCustomerInput, + WebflowCustomerOutput, +} from '@ecommerce/customer/services/webflow/types'; /* product */ export type OriginalProductInput = | ShopifyProductInput | WoocommerceProductInput - | SquarespaceProductInput | WebflowProductInput; + | SquarespaceProductInput + | WebflowProductInput; /* order */ export type OriginalOrderInput = | ShopifyOrderInput | WoocommerceOrderInput | SquarespaceOrderInput - | AmazonOrderInput; + | AmazonOrderInput + | WebflowOrderInput; /* fulfillmentorders */ export type OriginalFulfillmentOrdersInput = ShopifyFulfillmentOrdersInput; @@ -96,14 +103,16 @@ export type EcommerceObjectInput = export type OriginalProductOutput = | ShopifyProductOutput | WoocommerceProductOutput - | SquarespaceProductOutput | WebflowProductOutput; + | SquarespaceProductOutput + | WebflowProductOutput; /* order */ export type OriginalOrderOutput = | ShopifyOrderOutput | WoocommerceOrderOutput | SquarespaceOrderOutput - | AmazonOrderOutput; + | AmazonOrderOutput + | WebflowOrderOutput; /* fulfillmentorders */ export type OriginalFulfillmentOrdersOutput = ShopifyFulfillmentOrdersOutput; diff --git a/packages/api/src/ecommerce/customer/customer.module.ts b/packages/api/src/ecommerce/customer/customer.module.ts index 00b95c5f9..ceead802d 100644 --- a/packages/api/src/ecommerce/customer/customer.module.ts +++ b/packages/api/src/ecommerce/customer/customer.module.ts @@ -1,5 +1,3 @@ -import { WebflowCustomerMapper } from './services/webflow/mappers'; -import { WebflowService } from './services/webflow'; 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'; @@ -15,6 +13,8 @@ import { WoocommerceCustomerMapper } from './services/woocommerce/mappers'; import { SyncService } from './sync/sync.service'; import { SquarespaceCustomerMapper } from './services/squarespace/mappers'; import { AmazonCustomerMapper } from './services/amazon/mappers'; +import { WebflowService } from './services/webflow'; +import { WebflowCustomerMapper } from './services/webflow/mappers'; @Module({ controllers: [CustomerController], providers: [ diff --git a/packages/api/src/ecommerce/order/order.module.ts b/packages/api/src/ecommerce/order/order.module.ts index f74634825..568f01b47 100644 --- a/packages/api/src/ecommerce/order/order.module.ts +++ b/packages/api/src/ecommerce/order/order.module.ts @@ -10,6 +10,8 @@ import { ShopifyService } from './services/shopify'; import { ShopifyOrderMapper } from './services/shopify/mappers'; import { WoocommerceService } from './services/woocommerce'; import { WoocommerceOrderMapper } from './services/woocommerce/mappers'; +import { WebflowService } from './services/webflow'; +import { WebflowOrderMapper } from './services/webflow/mappers'; import { SyncService } from './sync/sync.service'; import { SquarespaceService } from './services/squarespace'; import { SquarespaceOrderMapper } from './services/squarespace/mappers'; @@ -35,6 +37,8 @@ import { AmazonService } from './services/amazon'; WoocommerceService, SquarespaceService, AmazonService, + WebflowService, + WebflowOrderMapper, ], exports: [SyncService], }) diff --git a/packages/api/src/ecommerce/product/product.module.ts b/packages/api/src/ecommerce/product/product.module.ts index ac43271e3..ef8633954 100644 --- a/packages/api/src/ecommerce/product/product.module.ts +++ b/packages/api/src/ecommerce/product/product.module.ts @@ -1,5 +1,3 @@ -import { WebflowProductMapper } from './services/webflow/mappers'; -import { WebflowService } from './services/webflow'; 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'; @@ -14,6 +12,8 @@ import { SquarespaceService } from './services/squarespace'; import { SquarespaceProductMapper } from './services/squarespace/mappers'; import { WoocommerceService } from './services/woocommerce'; import { WoocommerceProductMapper } from './services/woocommerce/mappers'; +import { WebflowService } from './services/webflow'; +import { WebflowProductMapper } from './services/webflow/mappers'; import { SyncService } from './sync/sync.service'; @Module({ From 42302d9b7f772d5f5858f76d5a99f45c13104d36 Mon Sep 17 00:00:00 2001 From: mit <1mitccc@gmail.com> Date: Sun, 1 Sep 2024 14:11:59 +0530 Subject: [PATCH 10/11] Update customer types.ts for ecommerce > customer > webflow --- .../ecommerce/customer/services/webflow/types.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/api/src/ecommerce/customer/services/webflow/types.ts b/packages/api/src/ecommerce/customer/services/webflow/types.ts index a55c2da4f..6ac8c32a0 100644 --- a/packages/api/src/ecommerce/customer/services/webflow/types.ts +++ b/packages/api/src/ecommerce/customer/services/webflow/types.ts @@ -1,3 +1,5 @@ +// ref: https://docs.developers.webflow.com/data/reference/list-users + export interface WebflowCustomerInput { id: string; isEmailVerified: boolean; @@ -5,21 +7,26 @@ export interface WebflowCustomerInput { invitedOn: string; createdOn: string; lastLogin: string; - status: string; + status: 'invited' | 'verified' | 'unverified'; accessGroups: AccessGroup[]; data: UserData; } type AccessGroup = { slug: string; - type: string; + type: AccessGroupType; }; +enum AccessGroupType { + ADMIN = 'admin', // Assigned to the user via API or in the designer + ECOMMERCE = 'ecommerce', // Assigned to the user via an ecommerce purchase +} + type UserData = { name: string; email: string; - 'accept-privacy': boolean; - 'accept-communications': boolean; + acceptPrivacy: boolean; + acceptCommunications: boolean; [key: string]: any; }; From 465558325d8d75d24c1f01b0c00b7b8e764f2c6b Mon Sep 17 00:00:00 2001 From: mit <1mitccc@gmail.com> Date: Sun, 1 Sep 2024 14:59:08 +0530 Subject: [PATCH 11/11] Refactor some code --- .../src/ecommerce/customer/services/webflow/index.ts | 2 +- .../src/ecommerce/order/services/webflow/index.ts | 2 +- .../src/ecommerce/order/services/webflow/mappers.ts | 2 +- .../src/ecommerce/product/services/webflow/index.ts | 3 ++- .../ecommerce/product/services/webflow/mappers.ts | 7 +++---- .../src/ecommerce/product/services/webflow/types.ts | 12 ++++++------ 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/api/src/ecommerce/customer/services/webflow/index.ts b/packages/api/src/ecommerce/customer/services/webflow/index.ts index 1256429a8..54c747341 100644 --- a/packages/api/src/ecommerce/customer/services/webflow/index.ts +++ b/packages/api/src/ecommerce/customer/services/webflow/index.ts @@ -19,7 +19,7 @@ export class WebflowService implements ICustomerService { private registry: ServiceRegistry, ) { this.logger.setContext( - EcommerceObject.customer.toUpperCase() + ':' + WebflowService.name, + `${EcommerceObject.customer.toUpperCase()}:${WebflowService.name}`, ); this.registry.registerService('webflow', this); } diff --git a/packages/api/src/ecommerce/order/services/webflow/index.ts b/packages/api/src/ecommerce/order/services/webflow/index.ts index 964fe2bc2..b3d1c5ca3 100644 --- a/packages/api/src/ecommerce/order/services/webflow/index.ts +++ b/packages/api/src/ecommerce/order/services/webflow/index.ts @@ -19,7 +19,7 @@ export class WebflowService implements IOrderService { private registry: ServiceRegistry, ) { this.logger.setContext( - EcommerceObject.order.toUpperCase() + ':' + WebflowService.name, + `${EcommerceObject.order.toUpperCase()}:${WebflowService.name}`, ); this.registry.registerService('webflow', this); } diff --git a/packages/api/src/ecommerce/order/services/webflow/mappers.ts b/packages/api/src/ecommerce/order/services/webflow/mappers.ts index 40400168a..d3e4c3e35 100644 --- a/packages/api/src/ecommerce/order/services/webflow/mappers.ts +++ b/packages/api/src/ecommerce/order/services/webflow/mappers.ts @@ -59,7 +59,7 @@ export class WebflowOrderMapper implements IOrderMapper { remote_id: string; }[], ): Promise { - const result: Partial = { + const result: UnifiedEcommerceOrderOutput = { remote_id: source.orderId, remote_data: source, created_at: source.acceptedOn, diff --git a/packages/api/src/ecommerce/product/services/webflow/index.ts b/packages/api/src/ecommerce/product/services/webflow/index.ts index 5636377e7..92ebdb1b4 100644 --- a/packages/api/src/ecommerce/product/services/webflow/index.ts +++ b/packages/api/src/ecommerce/product/services/webflow/index.ts @@ -21,7 +21,7 @@ export class WebflowService implements IProductService { private registry: ServiceRegistry, ) { this.logger.setContext( - EcommerceObject.product.toUpperCase() + ':' + WebflowService.name, + `${EcommerceObject.product.toUpperCase()}:${WebflowService.name}`, ); this.registry.registerService('webflow', this); } @@ -51,6 +51,7 @@ export class WebflowService implements IProductService { }, }, ); + return { data: resp.data, message: 'Webflow product created', diff --git a/packages/api/src/ecommerce/product/services/webflow/mappers.ts b/packages/api/src/ecommerce/product/services/webflow/mappers.ts index 1a3bc479c..7f9c68d0e 100644 --- a/packages/api/src/ecommerce/product/services/webflow/mappers.ts +++ b/packages/api/src/ecommerce/product/services/webflow/mappers.ts @@ -99,10 +99,9 @@ export class WebflowProductMapper implements IProductMapper { return { remote_id: data.product.id, remote_data: data, - images_urls: - data.skus - ?.map((sku) => sku?.fieldData?.mainImage?.url) - .filter((url) => Boolean(url)) || [], + images_urls: (data.skus || []) + .map(({ fieldData: { mainImage } }) => mainImage?.url) + .filter(Boolean), description: data.product.fieldData?.description, tags: data.product.fieldData?.categories, created_at: data.product?.createdOn, diff --git a/packages/api/src/ecommerce/product/services/webflow/types.ts b/packages/api/src/ecommerce/product/services/webflow/types.ts index 773d7ac87..f6337ac07 100644 --- a/packages/api/src/ecommerce/product/services/webflow/types.ts +++ b/packages/api/src/ecommerce/product/services/webflow/types.ts @@ -5,6 +5,12 @@ export interface WebflowProductOutput { skus: SkuData[]; } +export interface WebflowProductInput { + product: ProductData; + sku: SkuData; + publishStatus?: 'staging' | 'live'; +} + export interface ProductData { id?: string; cmsLocaleId?: string; @@ -99,9 +105,3 @@ export interface DownloadFile { url: string; id: string; } - -export interface WebflowProductInput { - product: ProductData; - sku: SkuData; - publishStatus: 'staging' | 'live'; -}