diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index 80235b19e..795f6a6c7 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -1751,9 +1751,9 @@ model ecom_addresses { modified_at DateTime @db.Timestamptz(6) created_at DateTime @db.Timestamptz(6) remote_deleted Boolean - id_ecom_order String @db.Uuid + id_ecom_order String? @db.Uuid ecom_customers ecom_customers @relation(fields: [id_ecom_customer], references: [id_ecom_customer], onDelete: NoAction, onUpdate: NoAction, map: "fk_ecom_customer_customeraddress") - ecom_orders ecom_orders @relation(fields: [id_ecom_order], references: [id_ecom_order], onDelete: NoAction, onUpdate: NoAction, map: "fk_ecom_order_address") + ecom_orders ecom_orders? @relation(fields: [id_ecom_order], references: [id_ecom_order], onDelete: NoAction, onUpdate: NoAction, map: "fk_ecom_order_address") @@index([id_ecom_customer], map: "fk_index_ecom_customer_customeraddress") @@index([id_ecom_order], map: "fk_index_fk_ecom_order_address") diff --git a/packages/api/scripts/init.sql b/packages/api/scripts/init.sql index 591e1f8eb..0ec7ce94c 100644 --- a/packages/api/scripts/init.sql +++ b/packages/api/scripts/init.sql @@ -1533,7 +1533,7 @@ CREATE TABLE ecom_addresses modified_at timestamp with time zone NOT NULL, created_at timestamp with time zone NOT NULL, remote_deleted boolean NOT NULL, - id_ecom_order uuid NOT NULL, + id_ecom_order uuid NULL, CONSTRAINT PK_ecom_customer_addresses PRIMARY KEY ( id_ecom_address ), CONSTRAINT FK_ecom_customer_customeraddress FOREIGN KEY ( id_ecom_customer ) REFERENCES ecom_customers ( id_ecom_customer ), CONSTRAINT FK_ecom_order_address FOREIGN KEY ( id_ecom_order ) REFERENCES ecom_orders ( id_ecom_order ) diff --git a/packages/api/src/@core/connections/connections.controller.ts b/packages/api/src/@core/connections/connections.controller.ts index 64feb2b64..fb62fe273 100644 --- a/packages/api/src/@core/connections/connections.controller.ts +++ b/packages/api/src/@core/connections/connections.controller.ts @@ -95,7 +95,6 @@ export class ConnectionsController { // Step 3: Parse the JSON stateData = JSON.parse(decodedState); - console.log(stateData); } else if (state.includes('squarespace_delimiter')) { // squarespace asks for a random alphanumeric value // Split the random part and the base64 part @@ -108,7 +107,6 @@ export class ConnectionsController { } else { // If no HTML entities are present, parse directly stateData = JSON.parse(state); - console.log(stateData); } const { @@ -170,7 +168,6 @@ export class ConnectionsController { @Query('state') state: string, ) { try { - console.log(client_id) if (!account) throw new ReferenceError('account prop not found'); const params = `?client_id=${client_id}&response_type=${response_type}&redirect_uri=${redirect_uri}&state=${state}&nonce=${nonce}&scope=${scope}`; res.redirect(`https://${account}.gorgias.com/oauth/authorize${params}`); diff --git a/packages/api/src/@core/connections/crm/services/attio/attio.service.ts b/packages/api/src/@core/connections/crm/services/attio/attio.service.ts index 8f2e2dc13..1da8866e8 100644 --- a/packages/api/src/@core/connections/crm/services/attio/attio.service.ts +++ b/packages/api/src/@core/connections/crm/services/attio/attio.service.ts @@ -100,7 +100,7 @@ export class AttioConnectionService extends AbstractBaseConnectionService { vertical: 'crm', }, }); - if (isNotUnique) return; + //reconstruct the redirect URI that was passed in the frontend it must be the same const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( diff --git a/packages/api/src/@core/connections/crm/services/hubspot/hubspot.service.ts b/packages/api/src/@core/connections/crm/services/hubspot/hubspot.service.ts index a4b26c3d2..428dba4b7 100644 --- a/packages/api/src/@core/connections/crm/services/hubspot/hubspot.service.ts +++ b/packages/api/src/@core/connections/crm/services/hubspot/hubspot.service.ts @@ -97,7 +97,6 @@ export class HubspotConnectionService extends AbstractBaseConnectionService { vertical: 'crm', }, }); - if (isNotUnique) return; //reconstruct the redirect URI that was passed in the frontend it must be the same const REDIRECT_URI = `${ this.env.getDistributionMode() == 'selfhost' diff --git a/packages/api/src/@core/connections/crm/services/wealthbox/wealthbox.service.ts b/packages/api/src/@core/connections/crm/services/wealthbox/wealthbox.service.ts index 96715d3ef..04a5ebc31 100644 --- a/packages/api/src/@core/connections/crm/services/wealthbox/wealthbox.service.ts +++ b/packages/api/src/@core/connections/crm/services/wealthbox/wealthbox.service.ts @@ -101,11 +101,8 @@ export class WealthboxConnectionService extends AbstractBaseConnectionService { vertical: 'crm', }, }); - if (isNotUnique) return; //reconstruct the redirect URI that was passed in the frontend it must be the same - const REDIRECT_URI = `${ - this.env.getPanoraBaseUrl() - }/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, diff --git a/packages/api/src/@core/connections/ecommerce/services/amazon/amazon.service.ts b/packages/api/src/@core/connections/ecommerce/services/amazon/amazon.service.ts index cfd710aed..b66c7f2af 100644 --- a/packages/api/src/@core/connections/ecommerce/services/amazon/amazon.service.ts +++ b/packages/api/src/@core/connections/ecommerce/services/amazon/amazon.service.ts @@ -98,7 +98,6 @@ export class AmazonConnectionService extends AbstractBaseConnectionService { vertical: 'ecommerce', }, }); - if (isNotUnique) return; //reconstruct the redirect URI that was passed in the frontend it must be the same const REDIRECT_URI = `${ this.env.getDistributionMode() == 'selfhost' @@ -141,6 +140,8 @@ export class AmazonConnectionService extends AbstractBaseConnectionService { data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), + account_url: CONNECTORS_METADATA['ecommerce']['amazon'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), diff --git a/packages/api/src/@core/connections/ecommerce/services/faire/faire.service.ts b/packages/api/src/@core/connections/ecommerce/services/faire/faire.service.ts index 0ce6ea90d..93e74c215 100644 --- a/packages/api/src/@core/connections/ecommerce/services/faire/faire.service.ts +++ b/packages/api/src/@core/connections/ecommerce/services/faire/faire.service.ts @@ -104,7 +104,6 @@ export class FaireConnectionService extends AbstractBaseConnectionService { vertical: 'ecommerce', }, }); - if (isNotUnique) return; //reconstruct the redirect URI that was passed in the frontend it must be the same const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; diff --git a/packages/api/src/@core/connections/ecommerce/services/mercadolibre/mercadolibre.service.ts b/packages/api/src/@core/connections/ecommerce/services/mercadolibre/mercadolibre.service.ts index 61a3ba1ff..2c5d63a92 100644 --- a/packages/api/src/@core/connections/ecommerce/services/mercadolibre/mercadolibre.service.ts +++ b/packages/api/src/@core/connections/ecommerce/services/mercadolibre/mercadolibre.service.ts @@ -112,7 +112,6 @@ export class MercadolibreConnectionService extends AbstractBaseConnectionService vertical: 'ecommerce', }, }); - if (isNotUnique) return; //reconstruct the redirect URI that was passed in the frontend it must be the same const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; diff --git a/packages/api/src/@core/connections/ecommerce/services/squarespace/squarespace.service.ts b/packages/api/src/@core/connections/ecommerce/services/squarespace/squarespace.service.ts index 9d51be370..97d3263e5 100644 --- a/packages/api/src/@core/connections/ecommerce/services/squarespace/squarespace.service.ts +++ b/packages/api/src/@core/connections/ecommerce/services/squarespace/squarespace.service.ts @@ -98,7 +98,6 @@ export class SquarespaceConnectionService extends AbstractBaseConnectionService vertical: 'ecommerce', }, }); - if (isNotUnique) return; //reconstruct the redirect URI that was passed in the frontend it must be the same const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; diff --git a/packages/api/src/@core/connections/ecommerce/services/webflow/webflow.service.ts b/packages/api/src/@core/connections/ecommerce/services/webflow/webflow.service.ts index 60e1dad77..d36e0b87c 100644 --- a/packages/api/src/@core/connections/ecommerce/services/webflow/webflow.service.ts +++ b/packages/api/src/@core/connections/ecommerce/services/webflow/webflow.service.ts @@ -97,7 +97,6 @@ export class WebflowConnectionService extends AbstractBaseConnectionService { vertical: 'ecommerce', }, }); - if (isNotUnique) return; //reconstruct the redirect URI that was passed in the frontend it must be the same const REDIRECT_URI = `${ this.env.getDistributionMode() == 'selfhost' diff --git a/packages/api/src/@core/connections/ticketing/services/gitlab/gitlab.service.ts b/packages/api/src/@core/connections/ticketing/services/gitlab/gitlab.service.ts index 3fa3874ce..db3ca6dcd 100644 --- a/packages/api/src/@core/connections/ticketing/services/gitlab/gitlab.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/gitlab/gitlab.service.ts @@ -128,8 +128,6 @@ export class GitlabConnectionService extends AbstractBaseConnectionService { 'OAuth credentials : gitlab ticketing ' + JSON.stringify(data), ); - // console.log("Gitlab Credentials : ", data) - let db_res; const connection_token = uuidv4(); @@ -209,7 +207,6 @@ export class GitlabConnectionService extends AbstractBaseConnectionService { }, ); const data: GitlabOAuthResponse = res.data; - // console.log("Gitlab Credentials (In refresh) : ", data) await this.prisma.connections.update({ where: { id_connection: connectionId, 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 622771044..95589729e 100644 --- a/packages/api/src/@core/utils/types/original/original.ecommerce.ts +++ b/packages/api/src/@core/utils/types/original/original.ecommerce.ts @@ -1,5 +1,6 @@ /* INPUT */ +import { AmazonCustomerOutput } from '@ecommerce/customer/services/amazon/types'; import { ShopifyCustomerInput, ShopifyCustomerOutput, @@ -20,6 +21,10 @@ import { ShopifyFulfillmentOrdersInput, ShopifyFulfillmentOrdersOutput, } from '@ecommerce/fulfillmentorders/services/shopify/types'; +import { + AmazonOrderInput, + AmazonOrderOutput, +} from '@ecommerce/order/services/amazon/types'; import { ShopifyOrderInput, ShopifyOrderOutput, @@ -55,7 +60,8 @@ export type OriginalProductInput = export type OriginalOrderInput = | ShopifyOrderInput | WoocommerceOrderInput - | SquarespaceOrderInput; + | SquarespaceOrderInput + | AmazonOrderInput; /* fulfillmentorders */ export type OriginalFulfillmentOrdersInput = ShopifyFulfillmentOrdersInput; @@ -88,7 +94,8 @@ export type OriginalProductOutput = export type OriginalOrderOutput = | ShopifyOrderOutput | WoocommerceOrderOutput - | SquarespaceOrderOutput; + | SquarespaceOrderOutput + | AmazonOrderOutput; /* fulfillmentorders */ export type OriginalFulfillmentOrdersOutput = ShopifyFulfillmentOrdersOutput; @@ -97,7 +104,8 @@ export type OriginalFulfillmentOrdersOutput = ShopifyFulfillmentOrdersOutput; export type OriginalCustomerOutput = | ShopifyCustomerOutput | WoocommerceCustomerOutput - | SquarespaceCustomerOutput; + | SquarespaceCustomerOutput + | AmazonCustomerOutput; /* fulfillment */ export type OriginalFulfillmentOutput = ShopifyFulfillmentOutput; diff --git a/packages/api/src/ecommerce/@lib/@types/index.ts b/packages/api/src/ecommerce/@lib/@types/index.ts index 0c5b29ec3..c0261f61d 100644 --- a/packages/api/src/ecommerce/@lib/@types/index.ts +++ b/packages/api/src/ecommerce/@lib/@types/index.ts @@ -108,14 +108,14 @@ export class Address { @ApiProperty({ type: String, - enum: ['PERSONAL', 'WORK'], - example: 'PERSONAL', + enum: ['SHIPPING', 'BILLING'], + example: 'SHIPPING', nullable: true, description: - 'The address type. Authorized values are either PERSONAL or WORK.', + 'The address type. Authorized values are either SHIPPING or BILLING.', }) - @IsIn(['PERSONAL', 'WORK']) + @IsIn(['SHIPPING', 'BILLING']) @IsOptional() @IsString() - address_type?: string; + address_type?: 'SHIPPING' | 'BILLING' | string; } diff --git a/packages/api/src/ecommerce/customer/customer.module.ts b/packages/api/src/ecommerce/customer/customer.module.ts index 448deb41e..6abeee12b 100644 --- a/packages/api/src/ecommerce/customer/customer.module.ts +++ b/packages/api/src/ecommerce/customer/customer.module.ts @@ -11,6 +11,8 @@ import { ShopifyCustomerMapper } from './services/shopify/mappers'; import { WoocommerceService } from './services/woocommerce'; import { WoocommerceCustomerMapper } from './services/woocommerce/mappers'; import { SyncService } from './sync/sync.service'; +import { SquarespaceCustomerMapper } from './services/squarespace/mappers'; +import { AmazonCustomerMapper } from './services/amazon/mappers'; @Module({ controllers: [CustomerController], @@ -24,6 +26,8 @@ import { SyncService } from './sync/sync.service'; Utils, ShopifyCustomerMapper, WoocommerceCustomerMapper, + SquarespaceCustomerMapper, + AmazonCustomerMapper, /* PROVIDERS SERVICES */ ShopifyService, WoocommerceService, diff --git a/packages/api/src/ecommerce/customer/services/amazon/mappers.ts b/packages/api/src/ecommerce/customer/services/amazon/mappers.ts new file mode 100644 index 000000000..25dc110af --- /dev/null +++ b/packages/api/src/ecommerce/customer/services/amazon/mappers.ts @@ -0,0 +1,98 @@ +import { AmazonCustomerOutput } 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 AmazonCustomerMapper implements ICustomerMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService( + 'ecommerce', + 'customer', + 'amazon', + this, + ); + } + + async desunify( + source: UnifiedEcommerceCustomerInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + return; + } + + async unify( + source: AmazonCustomerOutput | AmazonCustomerOutput[], + 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 AmazonCustomerOutput + return Promise.all( + source.map((customer) => + this.mapSingleCustomerToUnified( + customer, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleCustomerToUnified( + customer: AmazonCustomerOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const result: UnifiedEcommerceCustomerOutput = { + remote_id: null, + remote_data: customer, + email: customer.BuyerEmail || null, + first_name: customer.BuyerName || null, + phone_number: null, + }; + if (customer.Address) { + const add = customer.Address; + result.addresses = [ + { + street_1: add.AddressLine1, + street_2: null, + city: add.City, + state: add.StateOrRegion, + postal_code: add.PostalCode, + country: add.CountryCode, + address_type: 'SHIPPING', + }, + ]; + } + + return result; + } +} diff --git a/packages/api/src/ecommerce/customer/services/amazon/types.ts b/packages/api/src/ecommerce/customer/services/amazon/types.ts new file mode 100644 index 000000000..aa0af7c42 --- /dev/null +++ b/packages/api/src/ecommerce/customer/services/amazon/types.ts @@ -0,0 +1,12 @@ +export interface AmazonCustomerOutput { + BuyerEmail: string; + BuyerName: string; + Address?: Partial<{ + Name: string; + AddressLine1: string; + City: string; + StateOrRegion: string; + PostalCode: string; + CountryCode: string; + }>; +} diff --git a/packages/api/src/ecommerce/customer/services/shopify/mappers.ts b/packages/api/src/ecommerce/customer/services/shopify/mappers.ts index aeb7a5665..2b5149841 100644 --- a/packages/api/src/ecommerce/customer/services/shopify/mappers.ts +++ b/packages/api/src/ecommerce/customer/services/shopify/mappers.ts @@ -96,7 +96,7 @@ export class ShopifyCustomerMapper implements ICustomerMapper { state: add.province, postal_code: add.zip, country: add.country, - address_type: add.default ? 'PERSONAL' : 'WORK', + address_type: add.default ? 'SHIPPING' : null, }); } } diff --git a/packages/api/src/ecommerce/customer/services/squarespace/index.ts b/packages/api/src/ecommerce/customer/services/squarespace/index.ts deleted file mode 100644 index 9fed77237..000000000 --- a/packages/api/src/ecommerce/customer/services/squarespace/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -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 { SquarespaceCustomerOutput } from './types'; - -@Injectable() -export class SquarespaceService implements ICustomerService { - constructor( - private prisma: PrismaService, - private logger: LoggerService, - private cryptoService: EncryptionService, - private registry: ServiceRegistry, - ) { - this.logger.setContext( - EcommerceObject.customer.toUpperCase() + ':' + SquarespaceService.name, - ); - this.registry.registerService('squarespace', this); - } - - async sync( - data: SyncParam, - ): Promise> { - try { - const { linkedUserId } = data; - - const connection = await this.prisma.connections.findFirst({ - where: { - id_linked_user: linkedUserId, - provider_slug: 'squarespace', - vertical: 'ecommerce', - }, - }); - const resp = await axios.get(`${connection.account_url}/1.0/profiles`, { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, - }, - }); - const customers: SquarespaceCustomerOutput[] = resp.data.Profiles; - this.logger.log(`Synced squarespace customers !`); - - return { - data: customers, - message: 'Squarespace customers retrieved', - statusCode: 200, - }; - } catch (error) { - throw error; - } - } -} diff --git a/packages/api/src/ecommerce/customer/services/squarespace/mappers.ts b/packages/api/src/ecommerce/customer/services/squarespace/mappers.ts index 15e2df6e1..63146c927 100644 --- a/packages/api/src/ecommerce/customer/services/squarespace/mappers.ts +++ b/packages/api/src/ecommerce/customer/services/squarespace/mappers.ts @@ -72,19 +72,32 @@ export class SquarespaceCustomerMapper implements ICustomerMapper { }[], ): Promise { const result: UnifiedEcommerceCustomerOutput = { - remote_id: customer.id?.toString(), + remote_id: null, remote_data: customer, email: customer.email || null, first_name: customer.firstName || null, last_name: customer.lastName || null, phone_number: null, - addresses: [], field_mappings: customFieldMappings?.reduce((acc, mapping) => { acc[mapping.slug] = customer[mapping.remote_id]; return acc; }, {} as Record) || {}, }; + if (customer.address) { + const add = customer.address; + result.addresses = [ + { + street_1: add.address1, + street_2: add.address2 || null, + city: add.city, + state: add.state, + postal_code: add.postalCode, + country: add.countryCode, + address_type: 'SHIPPING', + }, + ]; + } return result; } diff --git a/packages/api/src/ecommerce/customer/services/squarespace/types.ts b/packages/api/src/ecommerce/customer/services/squarespace/types.ts index fe8e04be2..31773c448 100644 --- a/packages/api/src/ecommerce/customer/services/squarespace/types.ts +++ b/packages/api/src/ecommerce/customer/services/squarespace/types.ts @@ -1,25 +1,16 @@ export interface SquarespaceCustomerInput { - id: string; firstName: string; lastName: string; email: string; - hasAccount: boolean; - isCustomer: boolean; - createdOn: string; - address: string | null; - acceptsMarketing: boolean; - transactionsSummary: TransactionsSummary; + address: { + address1: string; + address2: string | null; + city: string; + state: string; + countryCode: string; + postalCode: string; + phone: string; + }; } -type TransactionsSummary = { - firstOrderSubmittedOn: string | null; - lastOrderSubmittedOn: string | null; - orderCount: number; - totalOrderAmount: number | null; - totalRefundAmount: number | null; - firstDonationSubmittedOn: string | null; - lastDonationSubmittedOn: string | null; - donationCount: number; - totalDonationAmount: number | null; -}; export type SquarespaceCustomerOutput = Partial; diff --git a/packages/api/src/ecommerce/customer/sync/sync.service.ts b/packages/api/src/ecommerce/customer/sync/sync.service.ts index dcb7209fb..95b00397c 100644 --- a/packages/api/src/ecommerce/customer/sync/sync.service.ts +++ b/packages/api/src/ecommerce/customer/sync/sync.service.ts @@ -189,11 +189,12 @@ export class SyncService implements OnModuleInit, IBaseSync { data: data, }); } else { + console.log('data is ' + JSON.stringify(data)); return this.prisma.ecom_addresses.create({ data: { ...data, id_ecom_customer: existingCustomer.id_ecom_customer, - id_ecom_order: '', + id_ecom_order: null, remote_deleted: false, //id_connection: connection_id, }, diff --git a/packages/api/src/ecommerce/order/order.module.ts b/packages/api/src/ecommerce/order/order.module.ts index dee1439d7..f74634825 100644 --- a/packages/api/src/ecommerce/order/order.module.ts +++ b/packages/api/src/ecommerce/order/order.module.ts @@ -11,6 +11,10 @@ import { ShopifyOrderMapper } from './services/shopify/mappers'; import { WoocommerceService } from './services/woocommerce'; import { WoocommerceOrderMapper } from './services/woocommerce/mappers'; import { SyncService } from './sync/sync.service'; +import { SquarespaceService } from './services/squarespace'; +import { SquarespaceOrderMapper } from './services/squarespace/mappers'; +import { AmazonOrderMapper } from './services/amazon/mappers'; +import { AmazonService } from './services/amazon'; @Module({ controllers: [OrderController], @@ -24,9 +28,13 @@ import { SyncService } from './sync/sync.service'; Utils, ShopifyOrderMapper, WoocommerceOrderMapper, + SquarespaceOrderMapper, + AmazonOrderMapper, /* PROVIDERS SERVICES */ ShopifyService, WoocommerceService, + SquarespaceService, + AmazonService, ], exports: [SyncService], }) diff --git a/packages/api/src/ecommerce/order/services/amazon/index.ts b/packages/api/src/ecommerce/order/services/amazon/index.ts new file mode 100644 index 000000000..4b902a81c --- /dev/null +++ b/packages/api/src/ecommerce/order/services/amazon/index.ts @@ -0,0 +1,135 @@ +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 { AmazonOrderInput, AmazonOrderOutput } from './types'; + +type Marketplace = { + country: string; + marketplaceId: string; + countryCode: string; +}; + +const marketplaces: Marketplace[] = [ + { country: 'Canada', marketplaceId: 'A2EUQ1WTGCTBG2', countryCode: 'CA' }, + { + country: 'United States of America', + marketplaceId: 'ATVPDKIKX0DER', + countryCode: 'US', + }, + { country: 'Mexico', marketplaceId: 'A1AM78C64UM0Y8', countryCode: 'MX' }, + { country: 'Brazil', marketplaceId: 'A2Q3Y263D00KWC', countryCode: 'BR' }, + { country: 'Spain', marketplaceId: 'A1RKKUPIHCS9HS', countryCode: 'ES' }, + { + country: 'United Kingdom', + marketplaceId: 'A1F83G8C2ARO7P', + countryCode: 'UK', + }, + { country: 'France', marketplaceId: 'A13V1IB3VIYZZH', countryCode: 'FR' }, + { country: 'Belgium', marketplaceId: 'AMEN7PMS3EDWL', countryCode: 'BE' }, + { + country: 'Netherlands', + marketplaceId: 'A1805IZSGTT6HS', + countryCode: 'NL', + }, + { country: 'Germany', marketplaceId: 'A1PA6795UKMFR9', countryCode: 'DE' }, + { country: 'Italy', marketplaceId: 'APJ6JRA9NG5V4', countryCode: 'IT' }, + { country: 'Sweden', marketplaceId: 'A2NODRKZP88ZB9', countryCode: 'SE' }, + { + country: 'South Africa', + marketplaceId: 'AE08WJ6YKNBMC', + countryCode: 'ZA', + }, + { country: 'Poland', marketplaceId: 'A1C3SOZRARQ6R3', countryCode: 'PL' }, + { country: 'Egypt', marketplaceId: 'ARBP9OOSHTCHU', countryCode: 'EG' }, + { country: 'Turkey', marketplaceId: 'A33AVAJ2PDY3EV', countryCode: 'TR' }, + { + country: 'Saudi Arabia', + marketplaceId: 'A17E79C6D8DWNP', + countryCode: 'SA', + }, + { + country: 'United Arab Emirates', + marketplaceId: 'A2VIGQ35RCS4UG', + countryCode: 'AE', + }, + { country: 'India', marketplaceId: 'A21TJRUUN4KGV', countryCode: 'IN' }, + { country: 'Singapore', marketplaceId: 'A19VAU5U5O7RUS', countryCode: 'SG' }, + { country: 'Australia', marketplaceId: 'A39IBJ37TRP1C6', countryCode: 'AU' }, + { country: 'Japan', marketplaceId: 'A1VC38T7YXB528', countryCode: 'JP' }, +]; + +@Injectable() +export class AmazonService implements IOrderService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + EcommerceObject.order.toUpperCase() + ':' + AmazonService.name, + ); + this.registry.registerService('amazon', this); + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'amazon', + vertical: 'ecommerce', + }, + }); + const specificMarketplaceIds = marketplaces.map( + (marketplace) => marketplace.marketplaceId, + ); + const resp = await axios.get( + `${connection.account_url}/orders/v0/orders?MarketplaceIds=${specificMarketplaceIds}&CreatedAfter=2010-10-10`, + { + headers: { + 'Content-Type': 'application/json', + 'x-amz-access-token': this.cryptoService.decrypt( + connection.access_token, + ), + }, + }, + ); + const orders: AmazonOrderOutput[] = resp.data.payload.Orders; + const ordersWithLineItems = await Promise.all( + orders.map(async (order) => { + const res = await axios.get( + `${connection.account_url}/orders/${order.AmazonOrderId}/orderItems`, + { + headers: { + 'Content-Type': 'application/json', + 'x-amz-access-token': this.cryptoService.decrypt( + connection.access_token, + ), + }, + }, + ); + return { ...order, LineItems: res.data.payload.OrderItems }; + }), + ); + this.logger.log(`Synced amazon orders !`); + + return { + data: ordersWithLineItems, + message: 'Amazon orders retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/ecommerce/order/services/amazon/mappers.ts b/packages/api/src/ecommerce/order/services/amazon/mappers.ts new file mode 100644 index 000000000..fa3baa067 --- /dev/null +++ b/packages/api/src/ecommerce/order/services/amazon/mappers.ts @@ -0,0 +1,167 @@ +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 { AmazonOrderOutput, AmazonOrderInput } from './types'; +import { CurrencyCode } from '@@core/utils/types'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { UnifiedEcommerceCustomerOutput } from '@ecommerce/customer/types/model.unified'; +import { AmazonCustomerOutput } from '@ecommerce/customer/services/amazon/types'; +import { EcommerceObject } from '@ecommerce/@lib/@types'; + +@Injectable() +export class AmazonOrderMapper implements IOrderMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService('ecommerce', 'order', 'amazon', this); + } + + async desunify( + source: UnifiedEcommerceOrderInput, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + return; + } + + async unify( + source: AmazonOrderOutput | AmazonOrderOutput[], + 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( + order: AmazonOrderOutput, + connectionId: string, + customFieldMappings?: { + slug: string; + remote_id: string; + }[], + ): Promise { + const opts: any = {}; + + // insert the buyer in the customers table + if (order.BuyerInfo) { + const customers = await this.ingestService.ingestData< + UnifiedEcommerceCustomerOutput, + AmazonCustomerOutput + >( + [ + { + BuyerEmail: order.BuyerInfo.BuyerEmail, + BuyerName: order.BuyerInfo.BuyerName, + Address: order.ShippingAddress, + }, + ], + 'amazon', + connectionId, + 'ecommerce', + EcommerceObject.customer, + [], + ); + opts.customer_id = customers[0].id_ecom_customer; + } + if (order.BuyerInfo.PurchaseOrderNumber) { + opts.order_number = order.BuyerInfo.PurchaseOrderNumber; + } + if (order.LineItems) { + opts.items = order.LineItems.map((item) => { + return { + remote_id: item.OrderItemId, + sku: item.SellerSKU || null, + title: item.Title || null, + quantity: item.QuantityOrdered, + price: item.ItemPrice?.Amount || null, + total: ( + parseFloat(item.ItemPrice?.Amount || '0') * item.QuantityOrdered + ).toFixed(2), + taxable: !!item.ItemTax, + weight: item.Measurement.Value || undefined, + tax_lines: item.ItemTax + ? [ + { + title: 'Sales Tax', + price: item.ItemTax.Amount, + rate: 0, // Set appropriate tax rate if available + }, + ] + : [], + discount_allocations: item.PromotionDiscount + ? [ + { + amount: item.PromotionDiscount.Amount, + discount_application_index: 0, // Adjust as necessary + }, + ] + : [], + }; + }); + // Calculate total_tax and total_discount + const total_tax = opts.items.reduce((acc, item) => { + const itemTax = item.tax_lines.reduce( + (taxAcc, tax) => taxAcc + parseFloat(tax.price), + 0, + ); + return acc + itemTax; + }, 0); + + const total_discount = opts.items.reduce((acc, item) => { + const itemDiscount = item.discount_allocations.reduce( + (discountAcc, discount) => discountAcc + parseFloat(discount.amount), + 0, + ); + return acc + itemDiscount; + }, 0); + + opts.total_tax = total_tax.toFixed(2); + opts.total_discount = total_discount.toFixed(2); + } + return { + remote_id: order.AmazonOrderId, + remote_data: order, + order_status: order.OrderStatus ? order.OrderStatus.toUpperCase() : null, + payment_status: 'SUCCESS', + currency: (order.OrderTotal.CurrencyCode as CurrencyCode) || null, + total_price: order.OrderTotal?.Amount + ? parseFloat(order.OrderTotal?.Amount) + : null, + total_shipping: null, + fulfillment_status: null, + field_mappings: customFieldMappings?.reduce( + (acc, mapping) => ({ + ...acc, + [mapping.slug]: order[mapping.remote_id as keyof AmazonOrderOutput], + }), + {}, + ), + ...opts, + }; + } +} diff --git a/packages/api/src/ecommerce/order/services/amazon/types.ts b/packages/api/src/ecommerce/order/services/amazon/types.ts new file mode 100644 index 000000000..5dde85ac8 --- /dev/null +++ b/packages/api/src/ecommerce/order/services/amazon/types.ts @@ -0,0 +1,177 @@ +export type AmazonOrderInput = { + AmazonOrderId: string; + PurchaseDate: string; // ISO 8601 date string + LastUpdateDate: string; // ISO 8601 date string + OrderStatus: string; + FulfillmentChannel: string; + NumberOfItemsShipped: number; + NumberOfItemsUnshipped: number; + PaymentMethod: string; + PaymentMethodDetails: string[]; // Array of payment method details + MarketplaceId: string; + ShipmentServiceLevelCategory: string; + OrderType: string; + EarliestShipDate: string; // ISO 8601 date string + LatestShipDate: string; // ISO 8601 date string + IsBusinessOrder: boolean; + IsPrime: boolean; + IsAccessPointOrder: boolean; + IsGlobalExpressEnabled: boolean; + IsPremiumOrder: boolean; + IsSoldByAB: boolean; + IsIBA: boolean; + ShippingAddress: ShippingAddress; + BuyerInfo: BuyerInfo; + OrderTotal: { + CurrencyCode: string; + Amount: string; + }; +} & { + LineItems: OrderItem[]; +}; + +export type AmazonOrderOutput = Partial; + +type ShippingAddress = { + Name: string; + AddressLine1: string; + City: string; + StateOrRegion: string; + PostalCode: string; + CountryCode: string; +}; + +type BuyerTaxInfo = { + CompanyLegalName: string; +}; + +type BuyerInfo = { + BuyerEmail: string; + BuyerName: string; + BuyerTaxInfo: BuyerTaxInfo; + PurchaseOrderNumber: string; +}; + +type Money = { + CurrencyCode: string; // ISO 4217 currency code + Amount: string; // Monetary amount +}; + +type AssociatedItem = { + ASIN: string; + Title: string; +}; + +type ProductInfoDetail = { + NumberOfItems: string; +}; + +type PointsGrantedDetail = { + PointsNumber: number; + PointsMonetaryValue: Money; +}; + +type TaxCollection = { + Model: 'MarketplaceFacilitator'; + ResponsibleParty: 'Amazon Services, Inc.'; +}; + +type ItemBuyerInfo = { + BuyerCustomizedInfo: { + CustomizedURL: string; + }; + GiftWrapPrice: Money; + GiftWrapTax: Money; + GiftMessageText: string; + GiftWrapLevel: string; +}; + +type BuyerRequestedCancel = { + IsBuyerRequestedCancel: boolean; + BuyerCancelReason: string; +}; + +type SubstitutionPreferences = { + SubstitutionType: + | 'CUSTOMER_PREFERENCE' + | 'AMAZON_RECOMMENDED' + | 'DO_NOT_SUBSTITUTE'; + SubstitutionOptions: { + ASIN: string; + QuantityOrdered: number; + SellerSKU: string; + Title: string; + Measurement: Measurement; + }[]; +}; + +type Measurement = { + Unit: string; + Value: number; +}; + +type ShippingConstraints = { + PalletDelivery: 'MANDATORY'; +}; + +export type OrderItem = { + ASIN: string; // The Amazon Standard Identification Number (ASIN) of the item. + SellerSKU?: string; // The seller stock keeping unit (SKU) of the item. + OrderItemId: string; // An Amazon-defined order item identifier. + AssociatedItems?: AssociatedItem[]; // A list of associated items purchased with a product. + Title?: string; // The name of the item. + QuantityOrdered: number; // The number of items in the order. + QuantityShipped?: number; // The number of items shipped. + ProductInfo?: ProductInfoDetail; // Product information for the item. + PointsGranted?: PointsGrantedDetail; // Amazon Points granted with the purchase. + ItemPrice?: Money; // The selling price of the order item. + ShippingPrice?: Money; // The shipping price of the item. + ItemTax?: Money; // The tax on the item price. + ShippingTax?: Money; // The tax on the shipping price. + ShippingDiscount?: Money; // The discount on the shipping price. + ShippingDiscountTax?: Money; // The tax on the discount on the shipping price. + PromotionDiscount?: Money; // The total of all promotional discounts. + PromotionDiscountTax?: Money; // The tax on the total of all promotional discounts. + PromotionIds?: string[]; // A list of promotion identifiers. + CODFee?: Money; // The fee charged for COD service. + CODFeeDiscount?: Money; // The discount on the COD fee. + IsGift?: boolean; // Indicates whether the item is a gift. + ConditionNote?: string; // The condition of the item as described by the seller. + ConditionId?: + | 'New' + | 'Used' + | 'Collectible' + | 'Refurbished' + | 'Preorder' + | 'Club'; // The condition of the item. + ConditionSubtypeId?: + | 'New' + | 'Mint' + | 'Very Good' + | 'Good' + | 'Acceptable' + | 'Poor' + | 'Club' + | 'OEM' + | 'Warranty' + | 'Refurbished Warranty' + | 'Refurbished' + | 'Open Box' + | 'Any' + | 'Other'; // The subcondition of the item. + ScheduledDeliveryStartDate?: string; // The start date of the scheduled delivery window. + ScheduledDeliveryEndDate?: string; // The end date of the scheduled delivery window. + PriceDesignation?: 'BusinessPrice'; // Indicates a special price for Amazon Business orders. + TaxCollection?: TaxCollection; // Information about withheld taxes. + SerialNumberRequired?: boolean; // Indicates if the product type has a serial number. + IsTransparency?: boolean; // Indicates if the ASIN is enrolled in Transparency. + IossNumber?: string; // The IOSS number for the marketplace. + StoreChainStoreId?: string; // The store chain store identifier. + DeemedResellerCategory?: string; // The category of deemed reseller. + BuyerInfo?: ItemBuyerInfo; // A single item's buyer information. + BuyerRequestedCancel?: BuyerRequestedCancel; // Information about buyer requested cancellation. + SerialNumbers?: string[]; // A list of serial numbers for electronic products. + SubstitutionPreferences?: SubstitutionPreferences; // Substitution preferences for the order item. + Measurement?: Measurement; // Measurement information for the order item. + ShippingConstraints?: ShippingConstraints; // Shipping constraints applicable to this order. +}; diff --git a/packages/api/src/ecommerce/order/services/shopify/mappers.ts b/packages/api/src/ecommerce/order/services/shopify/mappers.ts index 841436ee3..c89adea02 100644 --- a/packages/api/src/ecommerce/order/services/shopify/mappers.ts +++ b/packages/api/src/ecommerce/order/services/shopify/mappers.ts @@ -31,7 +31,7 @@ export class ShopifyOrderMapper implements IOrderMapper { line_items: source.items.map((item) => ({ title: item.title, price: item.price, - grams: item.grams, + grams: item.weight, quantity: item.quantity, sku: item.sku, variant_title: item.variant_title, diff --git a/packages/api/src/ecommerce/order/services/shopify/types.ts b/packages/api/src/ecommerce/order/services/shopify/types.ts index c20d51ee4..1a25f5560 100644 --- a/packages/api/src/ecommerce/order/services/shopify/types.ts +++ b/packages/api/src/ecommerce/order/services/shopify/types.ts @@ -46,7 +46,7 @@ export interface ShopifyOrderInput { gateway: string; id: number; landing_site: string; - line_items: LineItem[]; + line_items: Partial[]; location_id: number; merchant_of_record_app_id: number; name: string; @@ -219,13 +219,13 @@ type LineItem = { value: string; }[]; taxable: boolean; - tax_lines: { + tax_lines: Partial<{ title: string; price: string; price_set: MoneySet; channel_liable: boolean; rate: number; - }[]; + }>[]; total_discount: string; total_discount_set: MoneySet; discount_allocations: { diff --git a/packages/api/src/ecommerce/order/services/squarespace/index.ts b/packages/api/src/ecommerce/order/services/squarespace/index.ts index 48865aca1..38dfc40d9 100644 --- a/packages/api/src/ecommerce/order/services/squarespace/index.ts +++ b/packages/api/src/ecommerce/order/services/squarespace/index.ts @@ -25,44 +25,6 @@ export class SquarespaceService implements IOrderService { this.registry.registerService('squarespace', this); } - async addOrder( - orderData: SquarespaceOrderInput, - linkedUserId: string, - ): Promise> { - try { - const connection = await this.prisma.connections.findFirst({ - where: { - id_linked_user: linkedUserId, - provider_slug: 'squarespace', - vertical: 'ecommerce', - }, - }); - const resp = await axios.post( - `${connection.account_url}/1.0/commerce/orders`, - { - order: orderData, - }, - { - headers: { - 'Content-Type': 'application/json', - 'Idempotency-Key': uuidv4(), - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, - }, - }, - ); - - return { - data: resp.data, - message: 'Squarespace order created', - statusCode: 201, - }; - } catch (error) { - throw error; - } - } - async sync(data: SyncParam): Promise> { try { const { linkedUserId } = data; diff --git a/packages/api/src/ecommerce/order/services/squarespace/mappers.ts b/packages/api/src/ecommerce/order/services/squarespace/mappers.ts index 0ab8461ba..b763b6227 100644 --- a/packages/api/src/ecommerce/order/services/squarespace/mappers.ts +++ b/packages/api/src/ecommerce/order/services/squarespace/mappers.ts @@ -9,12 +9,17 @@ import { CoreUnification } from '@@core/@core-services/unification/core-unificat import { Utils } from '@ecommerce/@lib/@utils'; import { SquarespaceOrderOutput, SquarespaceOrderInput } from './types'; import { CurrencyCode } from '@@core/utils/types'; +import { EcommerceObject } from '@ecommerce/@lib/@types'; +import { SquarespaceCustomerOutput } from '@ecommerce/customer/services/squarespace/types'; +import { UnifiedEcommerceCustomerOutput } from '@ecommerce/customer/types/model.unified'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; @Injectable() export class SquarespaceOrderMapper implements IOrderMapper { constructor( private mappersRegistry: MappersRegistry, private utils: Utils, + private ingestService: IngestDataService, private coreUnificationService: CoreUnification, ) { this.mappersRegistry.registerService( @@ -119,25 +124,39 @@ export class SquarespaceOrderMapper implements IOrderMapper { ): Promise { const opts: any = {}; if (order.customerEmail) { - const customer_id = await this.utils.getCustomerIdFromRemote( - order.customerEmail, + const customers = await this.ingestService.ingestData< + UnifiedEcommerceCustomerOutput, + SquarespaceCustomerOutput + >( + [ + { + firstName: order.shippingAddress.firstName, + lastName: order.shippingAddress.lastName, + address: order.shippingAddress, + email: order.customerEmail, + }, + ], + 'squarespace', connectionId, + 'ecommerce', + EcommerceObject.customer, + [], ); - if (customer_id) { - opts.customer_id = customer_id; + if (customers && customers[0]) { + opts.customer_id = customers[0].id_ecom_customer; } } return { remote_id: order.id?.toString(), remote_data: order, - order_status: order.fulfillmentStatus || '', + order_status: null, order_number: order.orderNumber?.toString() || '', - payment_status: order.fulfillmentStatus || '', + payment_status: 'PAID', currency: (order.subtotal.currency as CurrencyCode) || null, - total_price: parseFloat(order.subtotal.value || '0'), - total_discount: parseFloat(order.discountTotal?.value || '0'), - total_shipping: parseFloat(order.shippingTotal?.value || '0'), - total_tax: parseFloat(order.taxTotal?.value || '0'), + total_price: parseInt(order.subtotal.value || '0'), + total_discount: parseInt(order.discountTotal?.value || '0'), + total_shipping: parseInt(order.shippingTotal?.value || '0'), + total_tax: parseInt(order.taxTotal?.value || '0'), fulfillment_status: order.fulfillmentStatus || '', field_mappings: customFieldMappings?.reduce( (acc, mapping) => ({ @@ -151,20 +170,6 @@ export class SquarespaceOrderMapper implements IOrderMapper { }; } - private mapAddress(address: any) { - return { - firstName: address.firstName || '', - lastName: address.lastName || '', - address1: address.address1 || '', - address2: address.address2 || null, - city: address.city || '', - state: address.state || '', - countryCode: address.countryCode || '', - postalCode: address.postalCode || '', - phone: address.phone || '', - }; - } - private mapLineItems(items: any[]) { return items.map((item) => ({ variantId: item.variantId || '', diff --git a/packages/api/src/ecommerce/order/services/woocommerce/mappers.ts b/packages/api/src/ecommerce/order/services/woocommerce/mappers.ts index 4c41571aa..4d963dadf 100644 --- a/packages/api/src/ecommerce/order/services/woocommerce/mappers.ts +++ b/packages/api/src/ecommerce/order/services/woocommerce/mappers.ts @@ -59,7 +59,7 @@ export class WoocommerceOrderMapper implements IOrderMapper { quantity: item.quantity, tax_class: '', subtotal: item.price.toString(), - total: (item.price * item.quantity).toString(), + total: (parseFloat(item.price) * item.quantity).toString(), sku: item.sku, })) as any; } @@ -110,14 +110,14 @@ export class WoocommerceOrderMapper implements IOrderMapper { remote_data: order, order_status: this.mapWooCommerceStatusToUnified(order.status), order_number: order.number, - payment_status: order.status, // WooCommerce doesn't have a separate payment status + payment_status: order.status, currency: order.currency as CurrencyCode, total_price: parseFloat(order.total || '0'), total_discount: parseFloat(order.discount_total || '0'), total_shipping: parseFloat(order.shipping_total || '0'), total_tax: parseFloat(order.total_tax || '0'), fulfillment_status: order.status, // WooCommerce doesn't have a separate fulfillment status - items: {}, + items: [], field_mappings: {}, }; if (order.customer_id) { @@ -128,15 +128,13 @@ export class WoocommerceOrderMapper implements IOrderMapper { } if (order.line_items) { - result.items = order.line_items.reduce((acc, item) => { - acc[item.id.toString()] = { - title: item.name, - price: parseFloat(item.price), - quantity: item.quantity, - sku: item.sku, - }; - return acc; - }, {}); + result.items = order.line_items.map((item) => ({ + remote_id: item.id, + title: item.name, + price: item.price, + quantity: item.quantity, + sku: item.sku, + })); } if (customFieldMappings && order.meta_data) { @@ -156,16 +154,10 @@ export class WoocommerceOrderMapper implements IOrderMapper { status?: string, ): WoocommerceOrderInput['status'] { switch (status) { - case 'PAID': - return 'processing'; - case 'UNPAID': + case 'PENDING': return 'pending'; - case 'CANCELLED': - return 'cancelled'; - case 'REFUNDED': - return 'refunded'; default: - return 'pending'; + return status as WoocommerceOrderInput['status']; } } @@ -173,18 +165,10 @@ export class WoocommerceOrderMapper implements IOrderMapper { status?: WoocommerceOrderInput['status'], ): string { switch (status) { - case 'processing': - case 'completed': - return 'PAID'; case 'pending': - case 'on-hold': - return 'UNPAID'; - case 'cancelled': - return 'CANCELLED'; - case 'refunded': - return 'REFUNDED'; + return 'PENDING'; default: - return 'UNPAID'; + return status; } } } diff --git a/packages/api/src/ecommerce/order/types/index.ts b/packages/api/src/ecommerce/order/types/index.ts index 8dac7726d..f2ae668b7 100644 --- a/packages/api/src/ecommerce/order/types/index.ts +++ b/packages/api/src/ecommerce/order/types/index.ts @@ -8,7 +8,7 @@ import { ApiResponse } from '@@core/utils/types'; import { IBaseObjectService, SyncParam } from '@@core/utils/types/interface'; export interface IOrderService extends IBaseObjectService { - addOrder( + addOrder?( orderData: DesunifyReturnType, linkedUserId: string, ): Promise>; diff --git a/packages/api/src/ecommerce/order/types/model.unified.ts b/packages/api/src/ecommerce/order/types/model.unified.ts index 84be0f0a0..fa98aef8e 100644 --- a/packages/api/src/ecommerce/order/types/model.unified.ts +++ b/packages/api/src/ecommerce/order/types/model.unified.ts @@ -7,18 +7,50 @@ import { IsDateString, IsInt, IsObject, + IsIn, + IsEnum, } from 'class-validator'; +export class LineItem { + remote_id: string | number; // Common and essential identifier + product_id: string | number; // Identifier for the product + variant_id?: string | number; // Identifier for the variant, optional as it's not always present + sku: string | null; // Stock Keeping Unit, essential for inventory + title: string; // Name or title of the product + quantity: number; // Number of items ordered + price: string; // Price per unit, critical for financials + total: string; // Total price, often needed for order summaries + fulfillment_status?: string; // Status of fulfillment, important for tracking + requires_shipping: boolean; // Whether the item requires shipping + taxable: boolean; // Whether the item is subject to tax + weight?: number; // Weight of the item, important for shipping calculations + variant_title?: string; // Title of the variant, optional but useful + vendor?: string | null; // Vendor information, optional but useful + properties?: { name: string; value: string }[]; // Custom properties, important for customization + tax_lines?: { + title: string; + price: string; + rate: number; + }[]; // Tax details, essential for tax calculations + discount_allocations?: { + amount: string; + discount_application_index: number; + }[]; // Discount details, important for pricing +} + +export type OrderStatus = 'PENDING' | 'UNSHIPPED' | 'SHIPPED' | 'CANCELED'; +export type FulfillmentStatus = 'PENDING' | 'FULFILLED' | 'CANCELED'; export class UnifiedEcommerceOrderInput { @ApiPropertyOptional({ type: String, - example: 'PAID', + example: 'UNSHIPPED', + enum: ['PENDING', 'UNSHIPPED', 'SHIPPED', 'CANCELED'], nullable: true, description: 'The status of the order', }) - @IsString() + @IsIn(['PENDING', 'UNSHIPPED', 'SHIPPED', 'CANCELED']) @IsOptional() - order_status?: string; + order_status?: OrderStatus | string; @ApiPropertyOptional({ type: String, @@ -37,9 +69,9 @@ export class UnifiedEcommerceOrderInput { nullable: true, description: 'The payment status of the order', }) - @IsString() + @IsIn(['SUCCESS', 'FAIL']) @IsOptional() - payment_status?: string; + payment_status?: 'SUCCESS' | 'FAIL' | string; @ApiPropertyOptional({ type: String, @@ -49,6 +81,7 @@ export class UnifiedEcommerceOrderInput { description: 'The currency of the order. Authorized value must be of type CurrencyCode (ISO 4217)', }) + @IsEnum(CurrencyCode) @IsOptional() currency?: CurrencyCode; @@ -95,12 +128,13 @@ export class UnifiedEcommerceOrderInput { @ApiPropertyOptional({ type: String, nullable: true, - example: 'delivered', + example: 'PENDING', + enum: ['PENDING', 'FULFILLED', 'CANCELED'], description: 'The fulfillment status of the order', }) - @IsString() + @IsIn(['PENDING', 'FULFILLED', 'CANCELED']) @IsOptional() - fulfillment_status?: string; + fulfillment_status?: FulfillmentStatus | string; @ApiPropertyOptional({ type: String, @@ -113,14 +147,35 @@ export class UnifiedEcommerceOrderInput { customer_id?: string; @ApiPropertyOptional({ - type: Object, - nullable: true, - example: {}, + type: [LineItem], + nullable: true, + example: [ + { + remote_id: '12345', + product_id: 'prod_001', + variant_id: 'var_001', + sku: 'SKU123', + title: 'Sample Product', + quantity: 2, + price: '19.99', + total: '39.98', + fulfillment_status: 'PENDING', + requires_shipping: true, + taxable: true, + weight: 1.5, + variant_title: 'Size M', + vendor: 'Sample Vendor', + properties: [{ name: 'Color', value: 'Red' }], + tax_lines: [{ title: 'Sales Tax', price: '3.00', rate: 0.075 }], + discount_allocations: [ + { amount: '5.00', discount_application_index: 0 }, + ], + }, + ], description: 'The items in the order', }) - @IsObject() @IsOptional() - items?: Record; + items?: Partial[]; @ApiPropertyOptional({ type: Object, diff --git a/packages/api/src/ecommerce/product/product.module.ts b/packages/api/src/ecommerce/product/product.module.ts index a31eb3a45..feb69f6d7 100644 --- a/packages/api/src/ecommerce/product/product.module.ts +++ b/packages/api/src/ecommerce/product/product.module.ts @@ -1,17 +1,18 @@ +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { Utils } from '@ecommerce/@lib/@utils'; import { Module } from '@nestjs/common'; import { ProductController } from './product.controller'; import { ProductService } from './services/product.service'; import { ServiceRegistry } from './services/registry.service'; -import { SyncService } from './sync/sync.service'; -import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; -import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; -import { Utils } from '@ecommerce/@lib/@utils'; import { ShopifyService } from './services/shopify'; import { ShopifyProductMapper } from './services/shopify/mappers'; +import { SquarespaceService } from './services/squarespace'; +import { SquarespaceProductMapper } from './services/squarespace/mappers'; import { WoocommerceService } from './services/woocommerce'; import { WoocommerceProductMapper } from './services/woocommerce/mappers'; +import { SyncService } from './sync/sync.service'; @Module({ controllers: [ProductController], @@ -25,9 +26,11 @@ import { WoocommerceProductMapper } from './services/woocommerce/mappers'; Utils, ShopifyProductMapper, WoocommerceProductMapper, + SquarespaceProductMapper, /* PROVIDERS SERVICES */ ShopifyService, WoocommerceService, + SquarespaceService, ], exports: [SyncService], }) diff --git a/packages/api/src/ecommerce/product/services/squarespace/index.ts b/packages/api/src/ecommerce/product/services/squarespace/index.ts index 7439e2256..39ff44d2f 100644 --- a/packages/api/src/ecommerce/product/services/squarespace/index.ts +++ b/packages/api/src/ecommerce/product/services/squarespace/index.ts @@ -26,43 +26,6 @@ export class SquarespaceService implements IProductService { this.registry.registerService('squarespace', this); } - async addProduct( - productData: SquarespaceProductInput, - linkedUserId: string, - ): Promise> { - try { - const connection = await this.prisma.connections.findFirst({ - where: { - id_linked_user: linkedUserId, - provider_slug: 'squarespace', - vertical: 'ecommerce', - }, - }); - const resp = await axios.post( - `${connection.account_url}/1.1/commerce/products`, - { - product: productData, - }, - { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.cryptoService.decrypt( - connection.access_token, - )}`, - }, - }, - ); - - return { - data: resp.data, - message: 'Squarespace product created', - statusCode: 201, - }; - } catch (error) { - throw error; - } - } - async sync( data: SyncParam, ): Promise> { @@ -77,7 +40,7 @@ export class SquarespaceService implements IProductService { }, }); const resp = await axios.get( - `${connection.account_url}/1.1/commerce/products`, + `${connection.account_url}/1.1/commerce/products?type=PHYSICAL,DIGITAL`, { headers: { 'Content-Type': 'application/json', diff --git a/packages/api/src/ecommerce/product/services/squarespace/mappers.ts b/packages/api/src/ecommerce/product/services/squarespace/mappers.ts index 006c7487f..2369f698f 100644 --- a/packages/api/src/ecommerce/product/services/squarespace/mappers.ts +++ b/packages/api/src/ecommerce/product/services/squarespace/mappers.ts @@ -31,62 +31,7 @@ export class SquarespaceProductMapper implements IProductMapper { remote_id: string; }[], ): Promise { - const res: any = { - type: source.product_type?.toUpperCase() || 'PHYSICAL', // Defaulting to PHYSICAL - storePageId: - customFieldMappings?.find((mapping) => mapping.slug === 'storePageId') - ?.remote_id || '', - name: source.description?.split(' ')[0] || 'Default Name', // Assuming name is the first word of the description - description: source.description, - url: source.product_url || '', - tags: source.tags || [], - isVisible: source.product_status === 'ACTIVE', - seoOptions: { - title: source.description?.substring(0, 60) || 'SEO Title', - description: source.description || 'SEO Description', - }, - variantAttributes: source.variants?.map((v) => v.options) || [], - variants: source.variants?.map((variant) => ({ - sku: variant.sku, - pricing: { - basePrice: { currency: 'USD', value: variant.price.toString() }, - salePrice: { currency: 'USD', value: variant.price.toString() }, // Assuming no sale price - onSale: false, - }, - stock: { - quantity: variant.inventory_quantity, - unlimited: variant.inventory_quantity === -1, // Assuming -1 means unlimited - }, - attributes: { option1: variant.options }, - shippingMeasurements: { - weight: { unit: 'POUND', value: variant.weight }, - dimensions: { - unit: 'INCH', - length: 0, - width: 0, - height: 0, - }, - }, - image: { - id: '', - altText: '', - url: '', - originalSize: { width: 0, height: 0 }, - availableFormats: [], - }, - })), - images: - source.images_urls?.map((url) => ({ - altText: 'Product Image', - url: url, - originalSize: { width: 0, height: 0 }, - availableFormats: [], - })) || [], - createdOn: new Date().toISOString(), - modifiedOn: new Date().toISOString(), - }; - - return res; + return; } async unify( diff --git a/packages/api/src/ecommerce/product/types/index.ts b/packages/api/src/ecommerce/product/types/index.ts index e76b4d93a..f73244f3a 100644 --- a/packages/api/src/ecommerce/product/types/index.ts +++ b/packages/api/src/ecommerce/product/types/index.ts @@ -8,7 +8,7 @@ import { ApiResponse } from '@@core/utils/types'; import { IBaseObjectService, SyncParam } from '@@core/utils/types/interface'; export interface IProductService extends IBaseObjectService { - addProduct( + addProduct?( productData: DesunifyReturnType, linkedUserId: string, ): Promise>; diff --git a/packages/api/swagger/swagger-spec.yaml b/packages/api/swagger/swagger-spec.yaml index bffc92674..e9cfb326b 100644 --- a/packages/api/swagger/swagger-spec.yaml +++ b/packages/api/swagger/swagger-spec.yaml @@ -14636,12 +14636,20 @@ components: description: >- The custom field mappings of the object between the remote 3rd party & Panora + LineItem: + type: object + properties: {} UnifiedEcommerceOrderOutput: type: object properties: order_status: type: string - example: PAID + example: UNSHIPPED + enum: &ref_150 + - PENDING + - UNSHIPPED + - SHIPPED + - CANCELED nullable: true description: The status of the order order_number: @@ -14652,7 +14660,7 @@ components: payment_status: type: string example: SUCCESS - enum: &ref_150 + enum: &ref_151 - SUCCESS - FAIL nullable: true @@ -14661,7 +14669,7 @@ components: type: string nullable: true example: AUD - enum: &ref_151 + enum: &ref_152 - AED - AFN - ALL @@ -14850,7 +14858,11 @@ components: fulfillment_status: type: string nullable: true - example: delivered + example: PENDING + enum: &ref_153 + - PENDING + - FULFILLED + - CANCELED description: The fulfillment status of the order customer_id: type: string @@ -14858,13 +14870,39 @@ components: nullable: true description: The UUID of the customer associated with the order items: - type: object nullable: true - example: &ref_152 {} + example: &ref_154 + - remote_id: '12345' + product_id: prod_001 + variant_id: var_001 + sku: SKU123 + title: Sample Product + quantity: 2 + price: '19.99' + total: '39.98' + fulfillment_status: PENDING + requires_shipping: true + taxable: true + weight: 1.5 + variant_title: Size M + vendor: Sample Vendor + properties: + - name: Color + value: Red + tax_lines: + - title: Sales Tax + price: '3.00' + rate: 0.075 + discount_allocations: + - amount: '5.00' + discount_application_index: 0 description: The items in the order + type: array + items: + $ref: '#/components/schemas/LineItem' field_mappings: type: object - example: &ref_153 + example: &ref_155 fav_dish: broccoli fav_color: red nullable: true @@ -14903,7 +14941,8 @@ components: properties: order_status: type: string - example: PAID + example: UNSHIPPED + enum: *ref_150 nullable: true description: The status of the order order_number: @@ -14914,14 +14953,14 @@ components: payment_status: type: string example: SUCCESS - enum: *ref_150 + enum: *ref_151 nullable: true description: The payment status of the order currency: type: string nullable: true example: AUD - enum: *ref_151 + enum: *ref_152 description: >- The currency of the order. Authorized value must be of type CurrencyCode (ISO 4217) @@ -14948,7 +14987,8 @@ components: fulfillment_status: type: string nullable: true - example: delivered + example: PENDING + enum: *ref_153 description: The fulfillment status of the order customer_id: type: string @@ -14956,13 +14996,15 @@ components: nullable: true description: The UUID of the customer associated with the order items: - type: object nullable: true - example: *ref_152 + example: *ref_154 description: The items in the order + type: array + items: + $ref: '#/components/schemas/LineItem' field_mappings: type: object - example: *ref_153 + example: *ref_155 nullable: true description: >- The custom field mappings of the object between the remote 3rd party @@ -15139,7 +15181,7 @@ components: field_mappings: type: object nullable: true - example: &ref_154 + example: &ref_156 fav_dish: broccoli fav_color: red description: >- @@ -15211,7 +15253,7 @@ components: field_mappings: type: object nullable: true - example: *ref_154 + example: *ref_156 description: >- The custom field mappings of the attachment between the remote 3rd party & Panora diff --git a/packages/shared/src/connectors/metadata.ts b/packages/shared/src/connectors/metadata.ts index 2b2086162..d7bda817f 100644 --- a/packages/shared/src/connectors/metadata.ts +++ b/packages/shared/src/connectors/metadata.ts @@ -2834,7 +2834,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { }, logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTu9U-j_3EMYlKtu5dRaTl6ejitL2X6lz3pYg&s', description: 'Sync & Create orders, fulfillments, fulfillment orders, customers and products', - active: false, + active: true, authStrategy: { strategy: AuthStrategy.oauth2, } @@ -2842,7 +2842,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { 'amazon': { urls: { docsUrl: 'https://developer-docs.amazon.com/sp-api/docs/welcome', - apiUrl: 'https://sandbox.sellingpartnerapi-na.amazon.com', // north america prod is https://sellingpartnerapi-na.amazon.com + apiUrl: 'https://sellingpartnerapi-na.amazon.com', authBaseUrl: 'https://sellercentral.amazon.com/apps/authorize/consent' }, logoPath: 'https://cdn.vectorstock.com/i/500p/39/87/astana-kazakhstan-20-july-2020-amazon-icon-vector-34243987.jpg',