Implementing Two-Way Catalog Synchronization with 1C
Two-way synchronization is not just import and export separately. It's managing conflicts: what happens when price changed both in 1C and on website simultaneously? Who is source of truth for specific field? Without clear rules synchronization becomes chaos.
Defining Sources of Truth
First step — establish for each field which system is master:
| Field | Master | Logic |
|---|---|---|
| Product name | 1C | 1C is nomenclature system |
| SKU / Article | 1C | Article set in accounting system |
| Description | Website | Marketing texts written in editor |
| Price | 1C | Pricing in accounting system |
| Stock | 1C | Actual warehouse accounting |
| Images | Website | Photos processed separately |
| SEO fields | Website | Meta title/description on website side |
| Active status | Both | 1C can delist, website too |
Database Schema
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
onec_guid UUID UNIQUE, -- 1C identifier
sku TEXT,
-- Fields from 1C (overwritten on each sync)
name_1c TEXT,
price_1c NUMERIC(12,2),
stock_1c INTEGER,
active_1c BOOLEAN DEFAULT true,
-- Website fields (not overwritten by sync)
description TEXT,
meta_title TEXT,
meta_description TEXT,
images JSONB,
active_site BOOLEAN DEFAULT true,
-- Sync metadata
last_sync_1c TIMESTAMPTZ,
sync_hash_1c CHAR(64) -- hash for change detection
);
-- Final status: product active only if active in both 1C AND website
CREATE VIEW products_active AS
SELECT * FROM products WHERE active_1c = true AND active_site = true;
Synchronization Algorithm: 1C → Website
class OnecToSiteSyncService
{
public function sync(CommerceMLData $data): SyncResult
{
$result = new SyncResult();
foreach ($data->products as $onecProduct) {
$syncHash = $this->computeHash($onecProduct);
$product = Product::firstOrNew(['onec_guid' => $onecProduct->guid]);
// Skip if data unchanged
if ($product->exists && $product->sync_hash_1c === $syncHash) {
$result->skipped++;
continue;
}
// Update ONLY fields from 1C (don't touch description, images, etc.)
$product->fill([
'sku' => $onecProduct->sku,
'name_1c' => $onecProduct->name,
'price_1c' => $onecProduct->price,
'stock_1c' => $onecProduct->stock,
'active_1c' => $onecProduct->active,
'last_sync_1c' => now(),
'sync_hash_1c' => $syncHash,
]);
$product->save();
$result->updated++;
}
// Products 1C no longer exports — deactivate
$syncedGuids = $data->products->pluck('guid');
Product::whereNotIn('onec_guid', $syncedGuids)
->update(['active_1c' => false]);
return $result;
}
}
Synchronization: Website → 1C
Website sends to 1C only what changed on website and matters for 1C: new orders, returns, payment confirmations.
class SiteToOnecSyncService
{
public function getUnsyncedOrders(): Collection
{
return Order::where('sent_to_1c', false)
->where('status', '!=', 'draft')
->with(['items.product', 'customer'])
->get();
}
}
Conflict Detection and Resolution
Conflict: active_site set to false by operator (delisted), but next 1C export contains active = true. Per rules table — 1C is master for active_1c, but active_site untouched.
Result: active_1c = true, active_site = false → product not displayed. Website operator retains control.
Monitoring Synchronization
-- Last sync statuses
SELECT
source,
COUNT(*) FILTER (WHERE status = 'success') AS success,
COUNT(*) FILTER (WHERE status = 'error') AS errors,
MAX(finished_at) AS last_run,
AVG(EXTRACT(EPOCH FROM (finished_at - started_at))) AS avg_duration_sec
FROM sync_logs
WHERE started_at > NOW() - INTERVAL '7 days'
GROUP BY source;
Timeline
Two-way catalog synchronization with 1C, including testing on real configuration: 14–20 work days.







