Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 51 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ACO SFCC Starter Kit

> [!Important]
> Installation of the custom ACO SFCC Cartridge is required: [int_adobe_commerce_optimizer](https://github.com/adobe-commerce/aco-sfcc-cartridges).
> [!Important] Installation of the custom ACO SFCC Cartridge is required:
> [int_adobe_commerce_optimizer](https://github.com/adobe-commerce/aco-sfcc-cartridges).

![Starter Kit Flow Diagram](./docs/images/diagram.png)

Expand Down Expand Up @@ -131,6 +131,8 @@ Run the following command to deploy your starter kit to your Developer Console p
aio app deploy
```

> [!TIP] Run the `aio app deploy` command with `--force-build --force-deploy` flags to force a clean build.

#### Onboard Your Starter Kit Actions

Run the following command to onboard the App Builder actions from your starter kit to your Developer Console project:
Expand Down Expand Up @@ -168,6 +170,7 @@ Entities Syncronized:

- Metadata
- Products
- Categories
- Price Books
- Prices

Expand All @@ -176,20 +179,41 @@ Entities Syncronized:
This action retrieves recent changes that have been made in SFCC since the last full or delta sync action and
synchonizes them with Commerce Optimizer.

By default, this action is scheduled to run every hour via the
By default, this action is scheduled to run every hour (`cron: 15 * * * *`) at 15 minutes past the hour using the
[App Builder Cron](https://developer.adobe.com/app-builder/docs/resources/cron-jobs/lesson2) action configuration.
Please adjust this schedule to align with the
[SFCC job configuration](https://github.com/adobe-commerce/aco-sfcc-cartridges#configure-the-adobecommerceoptimizertrackedchanges-job)
and to best fit your catalog data update frequency in the [App Configuration File](./app.config.yaml).

Example:

```yaml
triggers:
everyHour:
feed: /whisk.system/alarms/alarm
inputs:
cron: 15 * * * *
trigger_payload:
type: sfcc.delta.sync
data: {}
rules:
everyHourRule:
trigger: everyHour
action: delta-backoffice/consumer
```

Location: `actions/delta`

Entities Syncronized:

- Products
- Categories
- Price Books
- Prices

#### Price Book Sync

This action retrieves all price books in SFCC and syncronized them with Commerce Optimizer.
This action retrieves all price books in SFCC and syncronizes them with Commerce Optimizer.

Location: `actions/price-book`

Expand All @@ -204,6 +228,21 @@ Commerce Optimizer for each locale configured in the `SFCC_LOCALES_TO_SYNC` envi

Location: `actions/metadata`

#### Categories Sync

This action retrieves all categories in SFCC and syncronizes them with Commerce Optimizer.

Location: `actions/category`

Entities Syncronized:

- Categories

> [!NOTE] Only letters, numbers, and hyphens are allowed in ACO category slugs.

While syncronizing, the SFCC category ids are sanitized to remove special characters. Example:
`parent_cat/child_cat_1/child_cat_2/child_cat_3` becomes `parentcat/childcat1/childcat2/childcat3`

#### Specific Products Sync

This action retrieves product data for the provided SKUs (SFCC product IDs) from SFCC and synchronizes them with
Expand Down Expand Up @@ -348,6 +387,14 @@ The `lastMetadataSyncRun` key tracks the last time a metadata sync was successfu
- Example Value: `2025-07-24T00:13:45.341Z`
- Type: ISO 8601 String

#### Last Categories Sync Run

The `lastCategorySyncRun` key tracks the last time a category sync was successfully finished.

- Key: `lastCategorySyncRun`
- Example Value: `2025-07-24T00:13:45.341Z`
- Type: ISO 8601 String

#### Last Specific Product Sync Run

The `lastSpecificProductsSyncRun` key tracks the last time a specific product sync was successfully finished.
Expand Down
41 changes: 41 additions & 0 deletions actions/category/external/actions.config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
consumer:
function: consumer/index.js
web: 'no'
runtime: nodejs:22
annotations:
require-adobe-auth: true
final: true

sync:
function: sync/index.js
web: 'no'
runtime: nodejs:22
limits:
timeout: 10800000 # 3 hours
inputs:
LOG_LEVEL: debug
ACO_TENANT_ID: $ACO_TENANT_ID
ACO_REGION: $ACO_REGION
ACO_ENVIRONMENT_TYPE: $ACO_ENVIRONMENT_TYPE
SFCC_API_BASE_URL: $SFCC_API_BASE_URL
SFCC_REALM_ID: $SFCC_REALM_ID
SFCC_INSTANCE_ID: $SFCC_INSTANCE_ID
SFCC_ORGANIZATION_ID: $SFCC_ORGANIZATION_ID
SFCC_AUTH_URL: $SFCC_AUTH_URL
SFCC_CLIENT_ID: $SFCC_CLIENT_ID
SFCC_CLIENT_SECRET: $SFCC_CLIENT_SECRET
SFCC_SITE_ID: $SFCC_SITE_ID
SFCC_LOCALES_TO_SYNC: $SFCC_LOCALES_TO_SYNC
OAUTH_ORG_ID: $OAUTH_ORG_ID
OAUTH_CLIENT_ID: $OAUTH_CLIENT_ID
OAUTH_CLIENT_SECRET: $OAUTH_CLIENT_SECRET
OAUTH_TECHNICAL_ACCOUNT_ID: $OAUTH_TECHNICAL_ACCOUNT_ID
OAUTH_TECHNICAL_ACCOUNT_EMAIL: $OAUTH_TECHNICAL_ACCOUNT_EMAIL
IO_MANAGEMENT_BASE_URL: $IO_MANAGEMENT_BASE_URL
IO_CONSUMER_ID: $IO_CONSUMER_ID
IO_PROJECT_ID: $IO_PROJECT_ID
IO_WORKSPACE_ID: $IO_WORKSPACE_ID
AIO_runtime_namespace: $AIO_RUNTIME_NAMESPACE
annotations:
require-adobe-auth: true
final: true
75 changes: 75 additions & 0 deletions actions/category/external/consumer/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
Copyright 2025 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

const { Core } = require('@adobe/aio-sdk');
const { stringParameters, checkMissingRequestInputs } = require('../../../utils');
const { HTTP_INTERNAL_ERROR, HTTP_BAD_REQUEST } = require('../../../constants');
const Openwhisk = require('../../../openwhisk');
const { errorResponse, successResponse } = require('../../../responses');
const { instrumentConsumer } = require('../../../telemetry');

/**
* Handles the incoming category sync event and routes it to the appropiate runtime action.
*
* @param {CategoryActions.ConsumerEnv} params - The environment parameters.
* @returns {Promise<ActionResponse>} The response of the action.
*/
const main = async params => {
const logger = Core.Logger('category-external-consumer', {
level: params.LOG_LEVEL || 'info',
});

try {
const openwhiskClient = new Openwhisk(params.API_HOST, params.API_AUTH);

let activationId;

logger.info('Start processing request');
logger.debug(`Consumer main params: ${stringParameters(params)}`);

// check for missing request input parameters and headers
const requiredParams = ['type', 'data'];
const errorMessage = checkMissingRequestInputs(params, requiredParams, []);

if (errorMessage) {
logger.error(`Invalid request parameters: ${errorMessage}`);
return errorResponse(HTTP_BAD_REQUEST, `Invalid request parameters: ${errorMessage}`);
}

logger.info(`Params type: ${params.type}`);
switch (params.type) {
case 'sfcc.category.sync': {
logger.info('Invoking category sync');
activationId = await openwhiskClient.invokeAction('category-backoffice/sync', params.data);
break;
}
default: {
logger.error(`Event type not found: ${params.type}`);
return errorResponse(HTTP_BAD_REQUEST, `This case type is not supported: ${params.type}`);
}
}

if (!activationId) {
const message = `Error invoking action: ${params.type}`;
logger.error(message);
return errorResponse(HTTP_INTERNAL_ERROR, message);
}

logger.info(`Action ${params.type} invoked. Activation ID: ${activationId}`);
return successResponse(params.type, { activationId });
} catch (error) {
logger.error(`Server error: ${error.message}`);
return errorResponse(HTTP_INTERNAL_ERROR, error.message);
}
};

module.exports.main = instrumentConsumer(main);
58 changes: 58 additions & 0 deletions actions/category/external/sync/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
Copyright 2025 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

const { Core } = require('@adobe/aio-sdk');
const stateLib = require('@adobe/aio-lib-state');
const { AIO_STATE_MAX_TTL, AIO_STATE_KEY_LAST_CATEGORY_SYNC_RUN, HTTP_INTERNAL_ERROR } = require('../../../constants');
const { actionSuccessResponse, actionErrorResponse, StarterKitActionError } = require('../../../responses');
const { defineMain } = require('../../../telemetry');
const { configureAcoClient, configureSalesforceApiOptions, syncAllCategories } = require('../../../../api');

const main = async params => {
const logger = Core.Logger('sync-categories', {
level: params.LOG_LEVEL || 'info',
});

try {
logger.info(`Starting category sync for siteId: ${params.SFCC_SITE_ID}`);
logger.debug('Initializing AIO state lib');
const state = await stateLib.init();

/** @type {SalesforceApiOptions} */
const salesforceApiOptions = configureSalesforceApiOptions(params);
/** @type {AcoClientOptions} */
const acoClientOptions = configureAcoClient(params);
const localesToSync = params.SFCC_LOCALES_TO_SYNC.split(',');

logger.info(`Syncing all categories for siteId: ${params.SFCC_SITE_ID}`);
await syncAllCategories(
salesforceApiOptions,
acoClientOptions,
params.SFCC_ORGANIZATION_ID,
params.SFCC_SITE_ID,
localesToSync,
logger,
);

state.put(AIO_STATE_KEY_LAST_CATEGORY_SYNC_RUN, new Date().toISOString(), { ttl: AIO_STATE_MAX_TTL });
logger.info('Category sync completed successfully');
return actionSuccessResponse('Category sync completed successfully');
} catch (error) {
logger.error(`Error syncing categories to ACO: ${error}`);
if (error instanceof StarterKitActionError) {
return actionErrorResponse(error.status, error.message);
}
return actionErrorResponse(HTTP_INTERNAL_ERROR, error.message);
}
};

module.exports.main = defineMain(main);
2 changes: 2 additions & 0 deletions actions/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const AIO_STATE_KEY_LAST_SYNC = 'lastSyncTimestamp';
const AIO_STATE_KEY_LAST_FULL_SYNC_RUN = 'lastFullSyncRun';
const AIO_STATE_KEY_LAST_DELTA_SYNC_RUN = 'lastDeltaSyncRun';
const AIO_STATE_KEY_LAST_PRICE_BOOK_SYNC_RUN = 'lastPriceBookSyncRun';
const AIO_STATE_KEY_LAST_CATEGORY_SYNC_RUN = 'lastCategorySyncRun';
const AIO_STATE_KEY_LAST_METADATA_SYNC_RUN = 'lastMetadataSyncRun';
const AIO_STATE_KEY_LAST_SPECIFIC_PRODUCTS_SYNC_RUN = 'lastSpecificProductsSyncRun';
const AIO_STATE_KEY_FULL_SYNC_IN_PROGRESS = 'fullSyncInProgress';
Expand All @@ -44,6 +45,7 @@ module.exports = {
AIO_STATE_KEY_LAST_FULL_SYNC_RUN,
AIO_STATE_KEY_LAST_DELTA_SYNC_RUN,
AIO_STATE_KEY_LAST_PRICE_BOOK_SYNC_RUN,
AIO_STATE_KEY_LAST_CATEGORY_SYNC_RUN,
AIO_STATE_KEY_LAST_METADATA_SYNC_RUN,
AIO_STATE_KEY_LAST_SPECIFIC_PRODUCTS_SYNC_RUN,
AIO_STATE_KEY_FULL_SYNC_IN_PROGRESS,
Expand Down
30 changes: 11 additions & 19 deletions actions/full/external/sync/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,13 @@ const {
configureAcoClient,
configureSalesforceApiOptions,
createSalesforceAdminHttpClient,
getSalesforceSiteCatalogId,
getSiteCatalogId,
syncAllMetadata,
syncAllPriceBooks,
syncAllProducts,
syncAllCategories,
} = require('../../../../api');

const getSiteCatalogId = async (salesforceClient, salesforceOrgId, siteId, logger) => {
logger.debug(`Retrieving catalog id for site ${siteId} from Salesforce`);
const res = await getSalesforceSiteCatalogId(salesforceClient, salesforceOrgId, siteId);
if (!res.ok) {
if (res.status === 404) {
logger.error(`No catalog id is assigned to SFCC siteId: ${siteId}`);
} else {
logger.error(`Failed to retrieve catalog id from Salesforce: ${res.status} ${res.statusText}`);
}
throw new StarterKitActionError('Failed to retrieve catalog id from Salesforce', res.status, res.statusText);
}
/** @type {SalesforceSiteCatalogResponse} */
const data = await res.json();
const catalogId = data.id;
logger.debug(`Using catalog id ${catalogId} for site ${siteId}`);
return catalogId;
};

const fullSyncSite = async (params, logger) => {
/** @type {SalesforceApiOptions} */
const salesforceApiOptions = configureSalesforceApiOptions(params);
Expand All @@ -68,6 +51,15 @@ const fullSyncSite = async (params, logger) => {
logger,
);

await syncAllCategories(
salesforceApiOptions,
acoClientOptions,
params.SFCC_ORGANIZATION_ID,
params.SFCC_SITE_ID,
localesToSync,
logger,
);

for (const locale of localesToSync) {
await syncAllProducts(
salesforceApiOptions,
Expand Down
3 changes: 3 additions & 0 deletions actions/spa/last-sync-timestamps/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const { Core } = require('@adobe/aio-sdk');
const stateLib = require('@adobe/aio-lib-state');
const {
AIO_STATE_KEY_LAST_FULL_SYNC_RUN,
AIO_STATE_KEY_LAST_CATEGORY_SYNC_RUN,
AIO_STATE_KEY_LAST_DELTA_SYNC_RUN,
AIO_STATE_KEY_LAST_PRICE_BOOK_SYNC_RUN,
AIO_STATE_KEY_LAST_METADATA_SYNC_RUN,
Expand All @@ -35,13 +36,15 @@ const main = async params => {
const lastDeltaSyncRun = await state.get(AIO_STATE_KEY_LAST_DELTA_SYNC_RUN);
const lastPriceBookSyncRun = await state.get(AIO_STATE_KEY_LAST_PRICE_BOOK_SYNC_RUN);
const lastMetadataSyncRun = await state.get(AIO_STATE_KEY_LAST_METADATA_SYNC_RUN);
const lastCategorySyncRun = await state.get(AIO_STATE_KEY_LAST_CATEGORY_SYNC_RUN);
const lastSpecificProductsSyncRun = await state.get(AIO_STATE_KEY_LAST_SPECIFIC_PRODUCTS_SYNC_RUN);

const lastSyncTimestamps = {
lastFullSyncRun: lastFullSyncRun?.value,
lastDeltaSyncRun: lastDeltaSyncRun?.value,
lastPriceBookSyncRun: lastPriceBookSyncRun?.value,
lastMetadataSyncRun: lastMetadataSyncRun?.value,
lastCategorySyncRun: lastCategorySyncRun?.value,
lastSpecificProductsSyncRun: lastSpecificProductsSyncRun?.value,
};
return actionSuccessResponse('Last sync timestamps retrieved successfully', {
Expand Down
Loading