@@ -135,6 +83,42 @@ function BusinessCreatedModal({ business, onClose }) {
);
}
+function SalesChannelCard({ channel, disabled, onImport }) {
+ const name = getBusinessName(channel);
+ const domain = getBusinessDomain(channel);
+ const image = getBusinessImage(channel);
+
+ return (
+
+
+
+ {image ? (
+
+ ) : (
+
{name?.[0]?.toUpperCase() || 'S'}
+ )}
+
+
+
{name}
+
{domain || 'Domain unavailable'}
+
+
+
+
+ {channel.websiteUrl ? 'Ready to scrape' : 'Use manual URL fallback'}
+
+
+ {disabled ? 'Importing…' : 'Import'}
+
+
+
+ );
+}
+
export default function Businesses() {
const navigate = useNavigate();
const { setActiveBusiness } = useBusiness();
@@ -143,6 +127,7 @@ export default function Businesses() {
const [loading, setLoading] = useState(true);
const [salesChannelsStatus, setSalesChannelsStatus] = useState('loading');
const [salesChannelsError, setSalesChannelsError] = useState('');
+ const [salesChannelQuery, setSalesChannelQuery] = useState('');
const [selectingBusinessId, setSelectingBusinessId] = useState('');
const [creatingSalesChannelId, setCreatingSalesChannelId] = useState('');
const [createdBusiness, setCreatedBusiness] = useState(null);
@@ -163,6 +148,17 @@ export default function Businesses() {
salesChannels.filter((channel) => !configuredApplicationIds.has(getChannelId(channel)))
), [configuredApplicationIds, salesChannels]);
+ const filteredSalesChannels = useMemo(() => {
+ const query = salesChannelQuery.trim().toLowerCase();
+ if (!query) return availableSalesChannels;
+
+ return availableSalesChannels.filter((channel) => {
+ const name = String(getBusinessName(channel) || '').toLowerCase();
+ const domain = String(getBusinessDomain(channel) || '').toLowerCase();
+ return name.includes(query) || domain.includes(query);
+ });
+ }, [availableSalesChannels, salesChannelQuery]);
+
const loadBusinesses = useCallback(async () => {
const res = await apiClient.get('/api/businesses');
setBusinesses(res.data.businesses || []);
@@ -170,8 +166,7 @@ export default function Businesses() {
const loadSalesChannels = useCallback(async () => {
setSalesChannelsStatus('loading');
- const res = await apiClient.get('/api/businesses/sales-channels');
- const channels = normalizeChannelsPayload(res.data).filter(isChannelActive);
+ const channels = await fetchActiveSalesChannels();
setSalesChannels(channels);
setSalesChannelsStatus('success');
}, []);
@@ -195,7 +190,7 @@ export default function Businesses() {
setSalesChannels([]);
setSalesChannelsStatus('error');
setSalesChannelsError(
- salesChannelsRes.reason?.response?.data?.error || 'Active sales channels could not be loaded right now.'
+ salesChannelsRes.reason?.message || 'Active sales channels could not be loaded right now.'
);
}
} finally {
@@ -219,15 +214,23 @@ export default function Businesses() {
}
async function handleCreateFromSalesChannel(channel) {
- const salesChannelId = getChannelId(channel);
- if (!salesChannelId) return;
+ const applicationId = getChannelId(channel);
+ if (!applicationId) return;
- setCreatingSalesChannelId(salesChannelId);
+ if (!channel.websiteUrl) {
+ setSalesChannelsError('A website URL could not be derived from this sales channel. Please use Add Business and enter the URL manually.');
+ return;
+ }
+
+ setCreatingSalesChannelId(applicationId);
setError('');
setSalesChannelsError('');
try {
- const res = await apiClient.post('/api/businesses', { salesChannelId });
+ const res = await apiClient.post('/api/businesses', {
+ applicationId,
+ websiteUrl: channel.websiteUrl,
+ });
setCreatedBusiness(res.data);
await Promise.all([loadBusinesses(), loadSalesChannels()]);
setSalesChannelsStatus('success');
@@ -262,14 +265,14 @@ export default function Businesses() {
return (
-
+
{businesses.length > 0 ? 'Your Businesses' : 'Set Up Your First Business'}
- Add directly from an active sales channel, or use the existing fallback modal for manual testing.
+ Import from an active sales channel when available, or use the website URL fallback to scrape manually.
)}
-
-
-
-
Active Sales Channels
-
Choose a live channel to create its business record directly.
-
-
setShowModal(true)}
- className="text-sm font-semibold text-indigo-600 hover:text-indigo-700 transition"
- >
- Use fallback modal instead
-
-
-
- {salesChannelsError && (
-
- {salesChannelsError}
-
- )}
-
- {salesChannelsStatus === 'loading' ? (
-
-
-
Loading active sales channels…
-
- ) : availableSalesChannels.length > 0 ? (
-
- {availableSalesChannels.map((channel) => {
- const channelId = getChannelId(channel);
-
- return (
- handleCreateFromSalesChannel(channel)}
- disabled={creatingSalesChannelId === channelId}
- loadingLabel="Creating business from this sales channel…"
- footer={getBusinessDomain(channel) ? `Sales channel • ${getBusinessDomain(channel)}` : 'Sales channel ready'}
- />
- );
- })}
-
- ) : (
-
-
No active sales channels are available to add right now.
-
Already-configured channels are hidden here to avoid duplicate onboarding. You can still use the fallback Add Business modal for manual testing.
-
- )}
-
-
-
+
Configured Businesses
Select a business to manage its SMS templates.
@@ -402,19 +353,81 @@ export default function Businesses() {
) : (
No configured businesses yet.
-
Start from an active sales channel above, or use the Add Business modal as a fallback.
+
Import from an active sales channel below, or use Add Business to enter a storefront URL manually.
+
+ )}
+
+
+
+
+
+
Active Sales Channels
+
These are pulled directly from Commerce and can be scraped into businesses with one click.
+
+
setShowModal(true)}
+ className="text-sm font-semibold text-indigo-600 hover:text-indigo-700 transition"
+ >
+ Use website URL fallback
+
+
+
+ {salesChannelsError && (
+
+ {salesChannelsError}
+
+ )}
+
+ {salesChannelsStatus === 'loading' ? (
+
+
+
Loading active sales channels…
+
+ ) : availableSalesChannels.length > 0 ? (
+
+
+ Search Sales Channels
+ setSalesChannelQuery(e.target.value)}
+ placeholder="Search by channel name or domain"
+ className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm text-gray-900 placeholder-gray-400 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:border-transparent"
+ />
+
+
+
+ {filteredSalesChannels.map((channel) => {
+ const channelId = getChannelId(channel);
+ return (
+ handleCreateFromSalesChannel(channel)}
+ />
+ );
+ })}
+
+
+ {filteredSalesChannels.length === 0 && (
+
+
No active sales channels matched your search.
+
Use the website URL fallback if you want to scrape a storefront directly.
+
+ )}
+
+ ) : (
+
+
No active sales channels are available right now.
+
Use Add Business to enter a website URL manually and keep moving.
)}
{showModal && (
- { setShowModal(false); load(); }}
- initialSalesChannels={availableSalesChannels}
- initialSalesChannelsStatus={salesChannelsStatus}
- initialSalesChannelsError={salesChannelsError}
- />
+ { setShowModal(false); loadBusinesses(); }} />
)}
{createdBusiness && setCreatedBusiness(null)} />}
{deleteTarget && (
diff --git a/client/src/utils/businessProfile.js b/client/src/utils/businessProfile.js
index 6e06e94..b2c2385 100644
--- a/client/src/utils/businessProfile.js
+++ b/client/src/utils/businessProfile.js
@@ -2,11 +2,118 @@ function normalizeList(value) {
return Array.isArray(value) ? value.filter(Boolean) : [];
}
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : '';
+}
+
+function firstNonEmptyText(...values) {
+ for (const value of values) {
+ if (typeof value === 'string' && value.trim()) return value.trim();
+ }
+
+ return '';
+}
+
+function extractDomainName(domain) {
+ if (!domain) return '';
+ if (typeof domain === 'string') return normalizeText(domain);
+ if (typeof domain === 'object') {
+ return firstNonEmptyText(domain.name, domain.domain_url, domain.url, domain.host);
+ }
+ return '';
+}
+
+function rankDomain(domain) {
+ if (!domain || typeof domain !== 'object') return 0;
+
+ let score = 0;
+ if (domain.is_primary) score += 4;
+ if (domain.verified) score += 2;
+ if (!domain.is_shortlink) score += 1;
+ return score;
+}
+
+function pickPreferredDomain(...sources) {
+ const candidates = [];
+
+ for (const source of sources) {
+ if (!source) continue;
+
+ if (Array.isArray(source)) {
+ for (const item of source) {
+ const name = extractDomainName(item);
+ if (name) candidates.push({ raw: item, name });
+ }
+ continue;
+ }
+
+ const name = extractDomainName(source);
+ if (name) candidates.push({ raw: source, name });
+ }
+
+ if (!candidates.length) return '';
+
+ candidates.sort((left, right) => rankDomain(right.raw) - rankDomain(left.raw));
+ return candidates[0].name;
+}
+
+function normalizeWebsiteUrl(value) {
+ const rawValue = normalizeText(value);
+ if (!rawValue) return '';
+
+ const candidate = /^https?:\/\//i.test(rawValue) ? rawValue : `https://${rawValue}`;
+
+ try {
+ return new URL(candidate).toString().replace(/\/$/, '');
+ } catch {
+ return '';
+ }
+}
+
+function normalizeChannel(channel = {}) {
+ const domain = pickPreferredDomain(
+ channel.domain,
+ channel.domains,
+ channel.website,
+ channel.website_url,
+ channel.url
+ );
+ const id = String(channel?.salesChannelId || channel?.applicationId || channel?.application_id || channel?.id || channel?._id || '').trim();
+
+ return {
+ ...channel,
+ id,
+ salesChannelId: id,
+ applicationId: id,
+ name: firstNonEmptyText(channel.name, channel.display_name, channel.title, channel.slug),
+ domain,
+ websiteUrl: normalizeWebsiteUrl(channel.websiteUrl || channel.website_url || channel.url || domain),
+ isActive: channel.is_active !== false && channel.isActive !== false,
+ logoUrl: firstNonEmptyText(
+ channel.logoUrl,
+ channel.logo?.secure_url,
+ channel.logo?.url,
+ channel.logo,
+ channel.icon?.secure_url,
+ channel.icon?.url,
+ channel.icon,
+ channel.favicon?.secure_url,
+ channel.favicon?.url
+ ),
+ };
+}
+
export function normalizeChannelsPayload(data) {
- if (Array.isArray(data)) return data;
- if (Array.isArray(data?.salesChannels)) return data.salesChannels;
- if (Array.isArray(data?.channels)) return data.channels;
- return [];
+ const channels = (
+ Array.isArray(data) ? data
+ : Array.isArray(data?.salesChannels) ? data.salesChannels
+ : Array.isArray(data?.channels) ? data.channels
+ : Array.isArray(data?.items) ? data.items
+ : Array.isArray(data?.applications) ? data.applications
+ : []
+ );
+
+ return channels.map(normalizeChannel);
}
export function getChannelId(channel) {
diff --git a/client/src/utils/fyndSalesChannels.js b/client/src/utils/fyndSalesChannels.js
new file mode 100644
index 0000000..1fdeea2
--- /dev/null
+++ b/client/src/utils/fyndSalesChannels.js
@@ -0,0 +1,99 @@
+import { getRuntimeCompanyId, getRuntimeExtensionId } from './runtimeContext';
+import {
+ getChannelId,
+ isChannelActive,
+ normalizeChannelsPayload,
+} from './businessProfile';
+
+const FYND_PORTAL_API_BASE = 'https://api.fynd.com';
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : '';
+}
+
+function normalizeWebsiteUrl(value) {
+ const rawValue = normalizeText(value);
+ if (!rawValue) return '';
+
+ const candidate = /^https?:\/\//i.test(rawValue) ? rawValue : `https://${rawValue}`;
+
+ try {
+ return new URL(candidate).toString().replace(/\/$/, '');
+ } catch {
+ return '';
+ }
+}
+
+function readCookie(name) {
+ if (typeof document === 'undefined') return '';
+
+ const cookies = `; ${document.cookie || ''}`;
+ const parts = cookies.split(`; ${name}=`);
+ if (parts.length < 2) return '';
+ return parts.pop().split(';').shift() || '';
+}
+
+function buildPortalRequestUrl(companyId, extensionId) {
+ const search = new URLSearchParams({
+ company_id: companyId,
+ page_no: '1',
+ page_size: '100',
+ query: JSON.stringify({ is_active: true }),
+ });
+
+ if (extensionId) {
+ search.set('extension_id', extensionId);
+ }
+
+ return `${FYND_PORTAL_API_BASE}/service/portal/configuration/v1.0/company/${encodeURIComponent(companyId)}/application?${search.toString()}`;
+}
+
+function buildPortalHeaders() {
+ const headers = {
+ accept: 'application/json, text/plain, */*',
+ };
+
+ const token = normalizeText(readCookie('token'));
+ if (token) {
+ headers.authorization = `Bearer ${token}`;
+ }
+
+ return headers;
+}
+
+function withDerivedWebsiteUrl(channel) {
+ const explicitWebsiteUrl = normalizeWebsiteUrl(
+ channel?.websiteUrl || channel?.domain || channel?.url
+ );
+
+ return explicitWebsiteUrl
+ ? { ...channel, websiteUrl: explicitWebsiteUrl }
+ : channel;
+}
+
+export async function fetchActiveSalesChannels() {
+ const companyId = getRuntimeCompanyId();
+ const extensionId = getRuntimeExtensionId();
+
+ if (!companyId) {
+ throw new Error('Company ID is unavailable for fetching sales channels.');
+ }
+
+ const response = await fetch(buildPortalRequestUrl(companyId, extensionId), {
+ method: 'GET',
+ credentials: 'include',
+ headers: buildPortalHeaders(),
+ });
+
+ const payload = await response.json().catch(() => null);
+
+ if (!response.ok) {
+ const message = normalizeText(payload?.message || payload?.error)
+ || `Sales channels could not be fetched (${response.status}).`;
+ throw new Error(message);
+ }
+
+ return normalizeChannelsPayload(payload)
+ .map(withDerivedWebsiteUrl)
+ .filter((channel) => isChannelActive(channel) && getChannelId(channel));
+}
diff --git a/client/src/utils/runtimeContext.js b/client/src/utils/runtimeContext.js
index d547af4..5705cae 100644
--- a/client/src/utils/runtimeContext.js
+++ b/client/src/utils/runtimeContext.js
@@ -2,6 +2,7 @@ const COMPANY_ID_QUERY_KEYS = [
'blt-gtw-fc-cid',
];
const COMPANY_ID_STORAGE_KEY = 'sms_runtime_company_id';
+const EXTENSION_ID_STORAGE_KEY = 'sms_runtime_extension_id';
function getRuntimeUrl() {
if (typeof window === 'undefined') return null;
@@ -48,6 +49,26 @@ function persistCompanyId(companyId) {
}
}
+function readStoredExtensionId() {
+ if (typeof sessionStorage === 'undefined') return '';
+
+ try {
+ return (sessionStorage.getItem(EXTENSION_ID_STORAGE_KEY) || '').trim();
+ } catch {
+ return '';
+ }
+}
+
+function persistExtensionId(extensionId) {
+ if (!extensionId || typeof sessionStorage === 'undefined') return;
+
+ try {
+ sessionStorage.setItem(EXTENSION_ID_STORAGE_KEY, extensionId);
+ } catch {
+ // Ignore storage errors; URL/referrer extraction still works for the current request.
+ }
+}
+
function getFirstSearchParam(url, keys) {
if (!url) return '';
@@ -75,6 +96,20 @@ export function getRuntimeCompanyId() {
return companyId;
}
+export function getRuntimeExtensionId() {
+ const runtimeUrl = getRuntimeUrl();
+ const referrerUrl = getReferrerUrl();
+ const extensionId = (
+ getPathMatch(runtimeUrl?.pathname || '', /\/extensions\/([^/]+)/i)
+ || getPathMatch(referrerUrl?.pathname || '', /\/extensions\/([^/]+)/i)
+ || readStoredExtensionId()
+ || ''
+ );
+
+ persistExtensionId(extensionId);
+ return extensionId;
+}
+
export function getRuntimeApplicationId() {
const runtimeUrl = getRuntimeUrl();
if (!runtimeUrl) return '';
diff --git a/server/fdk.js b/server/fdk.js
index 941ba55..e7dcb8e 100644
--- a/server/fdk.js
+++ b/server/fdk.js
@@ -2,7 +2,6 @@ require('dotenv').config();
const { setupFdk } = require('@gofynd/fdk-extension-javascript/express');
const { MemoryStorage } = require('@gofynd/fdk-extension-javascript/express/storage');
-const { createFdkStorage } = require('./postgresFdkStorage');
function normalizeEnvText(value) {
return typeof value === 'string' ? value.trim() : '';
@@ -23,21 +22,11 @@ function createFdkExtension() {
const apiSecret = normalizeEnvText(process.env.EXTENSION_API_SECRET);
const baseUrl = normalizeEnvText(process.env.EXTENSION_BASE_URL || process.env.EXTENSION_URL);
const cluster = normalizeEnvText(process.env.EXTENSION_CLUSTER_URL) || 'https://api.fynd.com';
- const storageConnectionString = normalizeEnvText(process.env.FDK_STORAGE_CONNECTION_STRING);
if (!apiKey || !apiSecret || !baseUrl) {
return null;
}
- const storage = createFdkStorage({
- prefixKey: 'sms_extension_',
- connectionString: storageConnectionString,
- }) || new MemoryStorage('sms_extension_');
-
- if (!storageConnectionString) {
- console.warn('[FDK] FDK_STORAGE_CONNECTION_STRING is not set; falling back to in-memory session storage.');
- }
-
try {
return setupFdk({
api_key: apiKey,
@@ -52,8 +41,7 @@ function createFdkExtension() {
console.log(`[FDK] uninstall callback received for company ${companyId || 'unknown'}`);
},
},
- storage,
- access_mode: 'offline',
+ storage: new MemoryStorage('sms_extension_'),
});
} catch (error) {
console.warn(`[FDK] Failed to initialize FDK: ${error.message}`);
@@ -63,20 +51,7 @@ function createFdkExtension() {
const fdkExtension = createFdkExtension();
-async function getPlatformClientForCompany(companyId) {
- if (!fdkExtension) {
- throw new Error('FDK is not configured');
- }
-
- if (!normalizeEnvText(companyId)) {
- throw new Error('companyId is required to fetch a platform client');
- }
-
- return fdkExtension.getPlatformClient(companyId);
-}
-
module.exports = {
fdkExtension,
isFdkConfigured: Boolean(fdkExtension),
- getPlatformClientForCompany,
};
diff --git a/server/package-lock.json b/server/package-lock.json
index 49fc4aa..3970eb3 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -17,7 +17,6 @@
"express": "^4.18.2",
"multer": "^1.4.5-lts.1",
"openai": "^4.28.0",
- "pg": "^8.20.0",
"uuid": "^13.0.0"
},
"devDependencies": {
@@ -1669,96 +1668,6 @@
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
- "node_modules/pg": {
- "version": "8.20.0",
- "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
- "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "pg-connection-string": "^2.12.0",
- "pg-pool": "^3.13.0",
- "pg-protocol": "^1.13.0",
- "pg-types": "2.2.0",
- "pgpass": "1.0.5"
- },
- "engines": {
- "node": ">= 16.0.0"
- },
- "optionalDependencies": {
- "pg-cloudflare": "^1.3.0"
- },
- "peerDependencies": {
- "pg-native": ">=3.0.1"
- },
- "peerDependenciesMeta": {
- "pg-native": {
- "optional": true
- }
- }
- },
- "node_modules/pg-cloudflare": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
- "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/pg-connection-string": {
- "version": "2.12.0",
- "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
- "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
- "license": "MIT"
- },
- "node_modules/pg-int8": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
- "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
- "license": "ISC",
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/pg-pool": {
- "version": "3.13.0",
- "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
- "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
- "license": "MIT",
- "peerDependencies": {
- "pg": ">=8.0"
- }
- },
- "node_modules/pg-protocol": {
- "version": "1.13.0",
- "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
- "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
- "license": "MIT"
- },
- "node_modules/pg-types": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
- "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
- "license": "MIT",
- "dependencies": {
- "pg-int8": "1.0.1",
- "postgres-array": "~2.0.0",
- "postgres-bytea": "~1.0.0",
- "postgres-date": "~1.0.4",
- "postgres-interval": "^1.1.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/pgpass": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
- "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
- "license": "MIT",
- "dependencies": {
- "split2": "^4.1.0"
- }
- },
"node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
@@ -1772,45 +1681,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
- "node_modules/postgres-array": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
- "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/postgres-bytea": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
- "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/postgres-date": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
- "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/postgres-interval": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
- "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
- "license": "MIT",
- "dependencies": {
- "xtend": "^4.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -2137,15 +2007,6 @@
"node": ">=6"
}
},
- "node_modules/split2": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
- "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
- "license": "ISC",
- "engines": {
- "node": ">= 10.x"
- }
- },
"node_modules/stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
diff --git a/server/package.json b/server/package.json
index daab0b2..2b11c4d 100644
--- a/server/package.json
+++ b/server/package.json
@@ -17,7 +17,6 @@
"express": "^4.18.2",
"multer": "^1.4.5-lts.1",
"openai": "^4.28.0",
- "pg": "^8.20.0",
"uuid": "^13.0.0"
},
"devDependencies": {
diff --git a/server/postgresFdkStorage.js b/server/postgresFdkStorage.js
deleted file mode 100644
index 2b7dd80..0000000
--- a/server/postgresFdkStorage.js
+++ /dev/null
@@ -1,149 +0,0 @@
-const { Pool } = require('pg');
-const BaseStorage = require('@gofynd/fdk-extension-javascript/express/storage/base_storage');
-
-const DEFAULT_TABLE_NAME = 'fdk session storage';
-
-function normalizeEnvText(value) {
- return typeof value === 'string' ? value.trim() : '';
-}
-
-function quoteIdentifier(identifier) {
- return `"${String(identifier).replace(/"/g, '""')}"`;
-}
-
-function shouldUseSsl(connectionString) {
- const normalized = normalizeEnvText(connectionString).toLowerCase();
- if (!normalized) return false;
- if (normalized.includes('sslmode=disable')) return false;
- if (normalized.includes('sslmode=require')) return true;
- return !normalized.includes('localhost') && !normalized.includes('127.0.0.1');
-}
-
-function getPool(connectionString) {
- if (!globalThis.__smsExtensionFdkStoragePool) {
- globalThis.__smsExtensionFdkStoragePool = new Pool({
- connectionString,
- max: 3,
- idleTimeoutMillis: 30000,
- ssl: shouldUseSsl(connectionString) ? { rejectUnauthorized: false } : undefined,
- });
- }
-
- return globalThis.__smsExtensionFdkStoragePool;
-}
-
-function serializeValue(value) {
- return JSON.stringify(value);
-}
-
-function parseValue(rawValue) {
- if (!rawValue) return null;
- if (typeof rawValue === 'object') return rawValue;
-
- try {
- return JSON.parse(rawValue);
- } catch {
- return null;
- }
-}
-
-class PostgresFdkStorage extends BaseStorage {
- constructor({ prefixKey, connectionString, tableName = DEFAULT_TABLE_NAME }) {
- super(prefixKey);
-
- if (!normalizeEnvText(connectionString)) {
- throw new Error('FDK_STORAGE_CONNECTION_STRING is required for Postgres FDK storage');
- }
-
- this.pool = getPool(connectionString);
- this.tableIdentifier = quoteIdentifier(tableName);
- }
-
- buildKey(key) {
- return `${this.prefixKey}${key}`;
- }
-
- async get(key) {
- const storageKey = this.buildKey(key);
- const { rows } = await this.pool.query(
- `SELECT value, expires_at
- FROM ${this.tableIdentifier}
- WHERE storage_key = $1
- ORDER BY updated_at DESC NULLS LAST, created_at DESC NULLS LAST, id DESC
- LIMIT 1`,
- [storageKey]
- );
-
- const row = rows[0];
- if (!row) return null;
-
- const expiresAt = row.expires_at ? new Date(row.expires_at) : null;
- if (expiresAt && expiresAt.getTime() <= Date.now()) {
- await this.del(key);
- return null;
- }
-
- return parseValue(row.value);
- }
-
- async set(key, value) {
- await this.writeRecord(key, value, null);
- return 'OK';
- }
-
- async setex(key, value, ttl) {
- const expiresAt = Number.isFinite(ttl)
- ? new Date(Date.now() + ttl * 1000)
- : null;
-
- await this.writeRecord(key, value, expiresAt);
- return 'OK';
- }
-
- async del(key) {
- const storageKey = this.buildKey(key);
- await this.pool.query(
- `DELETE FROM ${this.tableIdentifier}
- WHERE storage_key = $1`,
- [storageKey]
- );
- }
-
- async writeRecord(key, value, expiresAt) {
- const storageKey = this.buildKey(key);
- const serializedValue = serializeValue(value);
-
- const updateResult = await this.pool.query(
- `UPDATE ${this.tableIdentifier}
- SET value = $2,
- expires_at = $3,
- updated_at = NOW()
- WHERE storage_key = $1`,
- [storageKey, serializedValue, expiresAt]
- );
-
- if (updateResult.rowCount > 0) return;
-
- await this.pool.query(
- `INSERT INTO ${this.tableIdentifier} (storage_key, value, expires_at)
- VALUES ($1, $2, $3)`,
- [storageKey, serializedValue, expiresAt]
- );
- }
-}
-
-function createFdkStorage({ prefixKey, connectionString }) {
- if (!normalizeEnvText(connectionString)) return null;
-
- return new PostgresFdkStorage({
- prefixKey,
- connectionString,
- tableName: DEFAULT_TABLE_NAME,
- });
-}
-
-module.exports = {
- DEFAULT_TABLE_NAME,
- PostgresFdkStorage,
- createFdkStorage,
-};
diff --git a/server/routes/businesses.js b/server/routes/businesses.js
index a4e3471..c865aad 100644
--- a/server/routes/businesses.js
+++ b/server/routes/businesses.js
@@ -13,7 +13,6 @@ const {
deleteBusinessFiles,
} = require('../services/pixelbin');
const DEFAULT_EVENTS = require('../config/defaultEvents');
-const { getPlatformClientForCompany } = require('../fdk');
const axios = require('axios');
const MERCHANT_ID = () => process.env.MERCHANT_ID;
@@ -216,69 +215,6 @@ function normalizeWebsiteUrl(value) {
}
}
-function extractDomainName(domain) {
- if (!domain) return '';
- if (typeof domain === 'string') return normalizeText(domain);
- if (typeof domain === 'object') {
- return firstNonEmptyText(domain.name, domain.domain_url, domain.url, domain.host);
- }
- return '';
-}
-
-function rankDomain(domain) {
- if (!domain || typeof domain !== 'object') return 0;
- let score = 0;
- if (domain.is_primary) score += 4;
- if (domain.verified) score += 2;
- if (!domain.is_shortlink) score += 1;
- return score;
-}
-
-function pickPreferredDomain(...sources) {
- const candidates = [];
-
- for (const source of sources) {
- if (!source) continue;
- if (Array.isArray(source)) {
- for (const item of source) {
- const name = extractDomainName(item);
- if (name) candidates.push({ raw: item, name });
- }
- continue;
- }
-
- const name = extractDomainName(source);
- if (name) candidates.push({ raw: source, name });
- }
-
- if (!candidates.length) return '';
-
- candidates.sort((left, right) => rankDomain(right.raw) - rankDomain(left.raw));
- return candidates[0].name;
-}
-
-function normalizeSalesChannel(application = {}, domains = []) {
- const domainName = pickPreferredDomain(application.domain, application.domains, domains);
- const id = normalizeScopeId(application._id || application.id || application.application_id);
-
- return {
- id,
- salesChannelId: id,
- applicationId: id,
- name: firstNonEmptyText(application.name, application.display_name, application.slug),
- domain: domainName,
- websiteUrl: normalizeWebsiteUrl(domainName),
- isActive: application.is_active !== false,
- logoUrl: firstNonEmptyText(
- application.logo?.secure_url,
- application.logo?.url,
- application.logo,
- application.favicon?.secure_url,
- application.favicon?.url
- ),
- };
-}
-
function getBusinessPreviewSummary(source = {}) {
const taglines = Array.isArray(source?.taglines)
? source.taglines.map((tagline) => normalizeText(tagline)).filter(Boolean)
@@ -303,58 +239,6 @@ function mergeBusinessSummary(baseBusiness = {}, context = null) {
};
}
-async function getPlatformClient(req, companyId) {
- if (req?.platformClient) return req.platformClient;
- return getPlatformClientForCompany(companyId);
-}
-
-async function listAllSalesChannels(platformClient, query = '') {
- const items = [];
- const pageSize = 100;
- let pageNo = 1;
-
- while (true) {
- const response = await platformClient.configuration.getApplications({
- pageNo,
- pageSize,
- q: normalizeText(query) || undefined,
- });
-
- const batch = Array.isArray(response?.items) ? response.items : [];
- items.push(...batch);
-
- const totalPages = Number(response?.page?.total_page) || Number(response?.page?.total_pages) || 0;
- if (!batch.length || batch.length < pageSize || (totalPages && pageNo >= totalPages)) {
- break;
- }
-
- pageNo += 1;
- }
-
- return items;
-}
-
-async function getSalesChannelDetails(platformClient, salesChannelId) {
- const applicationClient = platformClient.application(salesChannelId).configuration;
-
- const [applicationResponse, domainsResponse] = await Promise.allSettled([
- applicationClient.getApplicationById(),
- applicationClient.getDomains(),
- ]);
-
- if (applicationResponse.status !== 'fulfilled' && domainsResponse.status !== 'fulfilled') {
- const error = applicationResponse.reason || domainsResponse.reason || new Error('Unable to fetch sales channel details');
- throw error;
- }
-
- const application = applicationResponse.status === 'fulfilled'
- ? applicationResponse.value
- : { _id: salesChannelId };
- const domains = domainsResponse.status === 'fulfilled' ? domainsResponse.value?.domains || [] : [];
-
- return normalizeSalesChannel(application, domains);
-}
-
const LEGACY_DEFAULT_EVENT_SLUGS = new Set(['confirmed', 'pack', 'cancelled']);
const EVENT_TEMPLATE_FALLBACKS = {
bag_confirmed: ['confirmed'],
@@ -924,39 +808,7 @@ router.get('/', async (req, res) => {
}
});
-// GET /api/businesses/sales-channels
-router.get('/sales-channels', async (req, res) => {
- try {
- const companyId = getCompanyId(req);
- if (!companyId) {
- throw createHttpError(400, 'companyId is required');
- }
-
- const platformClient = await getPlatformClient(req, companyId);
- const applications = await listAllSalesChannels(platformClient);
- const salesChannels = applications
- .map((application) => normalizeSalesChannel(application))
- .filter((channel) => channel.id && channel.name && channel.isActive)
- .sort((left, right) => left.name.localeCompare(right.name));
-
- res.json({ salesChannels });
- } catch (err) {
- if (err.message === 'FDK is not configured') {
- return res.status(503).json({
- error: 'Sales channel fetch is unavailable',
- code: 'SALES_CHANNEL_FETCH_UNAVAILABLE',
- });
- }
-
- sendRouteError(res, createHttpError(
- err.status || 502,
- err.message || 'Failed to fetch sales channels',
- err.code ? { code: err.code, details: err.details } : {}
- ));
- }
-});
-
-// POST /api/businesses — create new business from sales channel or websiteUrl fallback
+// POST /api/businesses — create new business from websiteUrl with optional applicationId
router.post('/', async (req, res) => {
try {
const merchantId = getCompanyId(req);
@@ -964,51 +816,21 @@ router.post('/', async (req, res) => {
throw createHttpError(400, 'companyId is required');
}
- const requestedSalesChannelId = normalizeScopeId(
- req.body?.salesChannelId
- || req.body?.applicationId
+ const applicationId = normalizeScopeId(
+ req.body?.applicationId
|| req.body?.application_id
+ || req.body?.salesChannelId
+ || getApplicationId(req)
);
- let websiteUrl = normalizeWebsiteUrl(req.body?.websiteUrl);
- let salesChannel = null;
-
- if (!requestedSalesChannelId && !websiteUrl) {
- throw createHttpError(
- 400,
- 'Either salesChannelId or websiteUrl is required',
- { code: 'MISSING_BUSINESS_SOURCE' }
- );
- }
-
- if (requestedSalesChannelId) {
- try {
- const platformClient = await getPlatformClient(req, merchantId);
- salesChannel = await getSalesChannelDetails(platformClient, requestedSalesChannelId);
- } catch (error) {
- if (!websiteUrl) {
- throw createHttpError(
- error.message === 'FDK is not configured' ? 503 : 502,
- 'Unable to fetch sales channel details',
- {
- code: 'SALES_CHANNEL_DETAILS_UNAVAILABLE',
- details: { salesChannelId: requestedSalesChannelId, reason: error.message },
- }
- );
- }
- }
-
- websiteUrl = websiteUrl || salesChannel?.websiteUrl || '';
- }
+ const websiteUrl = normalizeWebsiteUrl(req.body?.websiteUrl);
if (!websiteUrl) {
throw createHttpError(
- 422,
- 'A website URL could not be derived from the selected sales channel. Please enter it manually.',
- { code: 'MISSING_SALES_CHANNEL_WEBSITE' }
+ 400,
+ 'websiteUrl is required',
+ { code: 'MISSING_WEBSITE_URL' }
);
}
-
- const applicationId = requestedSalesChannelId || getApplicationId(req);
const businesses = await getIndex(merchantId);
if (applicationId && businesses.some((business) => normalizeScopeId(business.applicationId) === applicationId)) {
@@ -1070,14 +892,7 @@ router.post('/', async (req, res) => {
});
await saveIndex(merchantId, businesses);
- res.json({
- ...contextJson,
- salesChannel: salesChannel ? {
- salesChannelId: salesChannel.id,
- name: salesChannel.name,
- domain: salesChannel.domain,
- } : null,
- });
+ res.json(contextJson);
} catch (err) {
console.error('Create business error:', err.message);
sendRouteError(res, err);