Skip to content

Commit

Permalink
Merge pull request #685 from amuwal/add-webflow-ecommerce-connector
Browse files Browse the repository at this point in the history
feat: Add integration with: Webflow (ecommerce)
  • Loading branch information
naelob authored Sep 3, 2024
2 parents e8c1bd4 + 5e42b21 commit dca151a
Show file tree
Hide file tree
Showing 18 changed files with 915 additions and 14 deletions.
3 changes: 2 additions & 1 deletion packages/api/scripts/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,8 @@ CREATE TABLE connector_sets
ecom_amazon boolean NULL,
ecom_squarespace boolean NULL,
ats_ashby boolean NULL,
crm_microsoftdynamicssales boolean NULL,
ecom_webflow boolean NULL,
crm_microsoftdynamicssales boolean NULL,
CONSTRAINT PK_project_connector PRIMARY KEY ( id_connector_set )
);

Expand Down
8 changes: 4 additions & 4 deletions packages/api/scripts/seed.sql
Original file line number Diff line number Diff line change
@@ -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','[email protected]', '$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, crm_microsoftdynamicssales) 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 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, crm_microsoftdynamicssales, 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, TRUE),
('852dfff8-ab63-4530-ae49-e4b2924407f8', TRUE, 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, 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'),
Expand Down
30 changes: 24 additions & 6 deletions packages/api/src/@core/utils/types/original/original.ecommerce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,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;
| SquarespaceProductInput
| WebflowProductInput;

/* order */
export type OriginalOrderInput =
| ShopifyOrderInput
| WoocommerceOrderInput
| SquarespaceOrderInput
| AmazonOrderInput;
| AmazonOrderInput
| WebflowOrderInput;

/* fulfillmentorders */
export type OriginalFulfillmentOrdersInput = ShopifyFulfillmentOrdersInput;
Expand All @@ -70,7 +84,8 @@ export type OriginalFulfillmentOrdersInput = ShopifyFulfillmentOrdersInput;
export type OriginalCustomerInput =
| ShopifyCustomerInput
| WoocommerceCustomerInput
| SquarespaceCustomerInput;
| SquarespaceCustomerInput
| WebflowCustomerInput;

/* fulfillment */
export type OriginalFulfillmentInput = ShopifyFulfillmentInput;
Expand All @@ -88,14 +103,16 @@ export type EcommerceObjectInput =
export type OriginalProductOutput =
| ShopifyProductOutput
| WoocommerceProductOutput
| SquarespaceProductOutput;
| SquarespaceProductOutput
| WebflowProductOutput;

/* order */
export type OriginalOrderOutput =
| ShopifyOrderOutput
| WoocommerceOrderOutput
| SquarespaceOrderOutput
| AmazonOrderOutput;
| AmazonOrderOutput
| WebflowOrderOutput;

/* fulfillmentorders */
export type OriginalFulfillmentOrdersOutput = ShopifyFulfillmentOrdersOutput;
Expand All @@ -105,7 +122,8 @@ export type OriginalCustomerOutput =
| ShopifyCustomerOutput
| WoocommerceCustomerOutput
| SquarespaceCustomerOutput
| AmazonCustomerOutput;
| AmazonCustomerOutput
| WebflowCustomerOutput;

/* fulfillment */
export type OriginalFulfillmentOutput = ShopifyFulfillmentOutput;
Expand Down
4 changes: 4 additions & 0 deletions packages/api/src/ecommerce/customer/customer.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,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: [
Expand All @@ -30,6 +32,8 @@ import { AmazonCustomerMapper } from './services/amazon/mappers';
/* PROVIDERS SERVICES */
ShopifyService,
WoocommerceService,
WebflowService,
WebflowCustomerMapper,
],
exports: [SyncService],
})
Expand Down
60 changes: 60 additions & 0 deletions packages/api/src/ecommerce/customer/services/webflow/index.ts
Original file line number Diff line number Diff line change
@@ -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<ApiResponse<WebflowCustomerOutput[]>> {
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;
}
}
}
91 changes: 91 additions & 0 deletions packages/api/src/ecommerce/customer/services/webflow/mappers.ts
Original file line number Diff line number Diff line change
@@ -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<WebflowCustomerInput> {
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<UnifiedEcommerceCustomerOutput> {
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<string, any>) || {},
};

return result;
}
}
33 changes: 33 additions & 0 deletions packages/api/src/ecommerce/customer/services/webflow/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// ref: https://docs.developers.webflow.com/data/reference/list-users

export interface WebflowCustomerInput {
id: string;
isEmailVerified: boolean;
lastUpdated: string;
invitedOn: string;
createdOn: string;
lastLogin: string;
status: 'invited' | 'verified' | 'unverified';
accessGroups: AccessGroup[];
data: UserData;
}

type AccessGroup = {
slug: 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;
acceptPrivacy: boolean;
acceptCommunications: boolean;
[key: string]: any;
};

export type WebflowCustomerOutput = Partial<WebflowCustomerInput>;
4 changes: 4 additions & 0 deletions packages/api/src/ecommerce/order/order.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -35,6 +37,8 @@ import { AmazonService } from './services/amazon';
WoocommerceService,
SquarespaceService,
AmazonService,
WebflowService,
WebflowOrderMapper,
],
exports: [SyncService],
})
Expand Down
58 changes: 58 additions & 0 deletions packages/api/src/ecommerce/order/services/webflow/index.ts
Original file line number Diff line number Diff line change
@@ -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<ApiResponse<WebflowOrderOutput[]>> {
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;
}
}
}
Loading

0 comments on commit dca151a

Please sign in to comment.