@porulle/plugin-channel-connector 0.50.0 → 0.50.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@porulle/plugin-channel-connector",
3
- "version": "0.50.0",
3
+ "version": "0.50.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -19,15 +19,15 @@
19
19
  "dependencies": {
20
20
  "@hono/zod-openapi": "^1.2.2",
21
21
  "hono": "^4.12.5",
22
- "@porulle/core": "0.50.0"
22
+ "@porulle/core": "0.50.1"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^24.5.2",
26
26
  "eslint": "^9.39.1",
27
27
  "typescript": "5.9.2",
28
28
  "vitest": "^3.2.4",
29
- "@porulle/typescript-config": "0.1.0",
30
- "@porulle/eslint-config": "0.1.0"
29
+ "@porulle/eslint-config": "0.1.0",
30
+ "@porulle/typescript-config": "0.1.0"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"
package/src/service.ts CHANGED
@@ -40,6 +40,8 @@ import {
40
40
  customerAddresses,
41
41
  customers,
42
42
  entityMedia,
43
+ entityBrands,
44
+ entityCategories,
43
45
  entityTags,
44
46
  inventoryLevels,
45
47
  mediaAssets,
@@ -1633,6 +1635,22 @@ export class ChannelConnectorService {
1633
1635
  existingOptionValues.set(row.variantId, list);
1634
1636
  }
1635
1637
  }
1638
+ // The product's plain base prices, read once. `setBasePrice` upserts and fires its hook even at
1639
+ // the same amount, so an unchanged price is skipped here — and a price that IS written is what
1640
+ // makes the variant "changed", which is what `converged` counts.
1641
+ const storedPrices = item.variants.some((variant) => (variant.prices ?? []).length > 0)
1642
+ ? await this.db.select({ variantId: prices.variantId, currency: prices.currency, amount: prices.amount, compareAtAmount: prices.compareAtAmount })
1643
+ .from(prices)
1644
+ .where(and(
1645
+ eq(prices.organizationId, orgId),
1646
+ eq(prices.entityId, entityId),
1647
+ isNull(prices.customerGroupId),
1648
+ isNull(prices.minQuantity),
1649
+ isNull(prices.maxQuantity),
1650
+ isNull(prices.validFrom),
1651
+ isNull(prices.validUntil),
1652
+ ))
1653
+ : [];
1636
1654
  for (const sourceVariant of item.variants) {
1637
1655
  const fullSourceVariant = fullItem.variants.find((variant) => variant.externalId === sourceVariant.externalId) ?? sourceVariant;
1638
1656
  let mapping = mappings.find((row) => row.externalId === sourceVariant.externalId);
@@ -1738,6 +1756,9 @@ export class ChannelConnectorService {
1738
1756
  }
1739
1757
  }
1740
1758
  for (const price of sourceVariant.prices ?? []) {
1759
+ const currency = price.currency.trim().toUpperCase();
1760
+ const stored = storedPrices.find((row) => row.variantId === variantId && row.currency === currency);
1761
+ if (stored && stored.amount === price.amount && stored.compareAtAmount === (price.compareAtAmount ?? null)) continue;
1741
1762
  const priced = await this.pricing.setBasePrice({
1742
1763
  entityId,
1743
1764
  variantId,
@@ -1746,6 +1767,7 @@ export class ChannelConnectorService {
1746
1767
  compareAtAmount: price.compareAtAmount ?? null,
1747
1768
  }, actor);
1748
1769
  if (!priced.ok) return PluginErr(priced.error.message);
1770
+ changed = true;
1749
1771
  }
1750
1772
  // Was UNCONDITIONAL: one UPDATE per variant on every import, including a re-sync where the
1751
1773
  // variant is byte-identical. `mapping.syncHash` is already in hand from the select above, so
@@ -1812,8 +1834,15 @@ export class ChannelConnectorService {
1812
1834
  item: ChannelCatalogItem,
1813
1835
  actor: Actor,
1814
1836
  warnings: string[],
1815
- ): Promise<PluginResult<void>> {
1837
+ ): Promise<PluginResult<{ changed: boolean }>> {
1816
1838
  const taxonomy = await this.taxonomyFor(orgId);
1839
+ // The links this entity already has, read only for the classes the item names. A link already
1840
+ // there is not re-written, and a link that is added is what reports the taxonomy as changed.
1841
+ const linkedCategories = new Set((item.categories ?? []).length === 0 ? [] : (await this.db
1842
+ .select({ id: entityCategories.categoryId }).from(entityCategories).where(eq(entityCategories.entityId, entityId))).map((row) => row.id));
1843
+ const linkedBrands = new Set(!item.brand ? [] : (await this.db
1844
+ .select({ id: entityBrands.brandId }).from(entityBrands).where(eq(entityBrands.entityId, entityId))).map((row) => row.id));
1845
+ let changed = false;
1817
1846
  const categoryRows = taxonomy.categories;
1818
1847
  for (const slug of new Set(item.categories ?? [])) {
1819
1848
  let category = categoryRows.find((row) => row.slug === slug);
@@ -1829,8 +1858,10 @@ export class ChannelConnectorService {
1829
1858
  category = createdCategory;
1830
1859
  categoryRows.push(category);
1831
1860
  }
1861
+ if (linkedCategories.has(category.id)) continue;
1832
1862
  const linked = await this.catalog.addToCategory(entityId, category.id, actor);
1833
1863
  if (!linked.ok) return PluginErr(linked.error.message);
1864
+ changed = true;
1834
1865
  }
1835
1866
 
1836
1867
  const brandRows = taxonomy.brands;
@@ -1844,8 +1875,11 @@ export class ChannelConnectorService {
1844
1875
  brand = createdBrand;
1845
1876
  brandRows.push(brand);
1846
1877
  }
1847
- const linked = await this.catalog.addToBrand(entityId, brand.id, actor);
1848
- if (!linked.ok) return PluginErr(linked.error.message);
1878
+ if (!linkedBrands.has(brand.id)) {
1879
+ const linked = await this.catalog.addToBrand(entityId, brand.id, actor);
1880
+ if (!linked.ok) return PluginErr(linked.error.message);
1881
+ changed = true;
1882
+ }
1849
1883
  }
1850
1884
 
1851
1885
  const tagRows = taxonomy.tags;
@@ -1860,9 +1894,10 @@ export class ChannelConnectorService {
1860
1894
  if (!tag) return PluginErr(`Tag "${slug}" was not persisted.`);
1861
1895
  tagRows.push(tag);
1862
1896
  }
1863
- await this.db.insert(entityTags).values({ entityId, tagId: tag.id }).onConflictDoNothing();
1897
+ const added = await this.db.insert(entityTags).values({ entityId, tagId: tag.id }).onConflictDoNothing().returning({ tagId: entityTags.tagId });
1898
+ if (added.length > 0) changed = true;
1864
1899
  }
1865
- return Ok(undefined);
1900
+ return Ok({ changed });
1866
1901
  }
1867
1902
 
1868
1903
  private async applyMedia(
@@ -3822,7 +3857,7 @@ export class ChannelConnectorService {
3822
3857
  mediaImported += media.value.imported;
3823
3858
  variantsGivenOptionValues += variantIds.value.repaired;
3824
3859
  skipped.push(...media.value.skipped.map((fieldPath) => ({ entityId, fieldPath })));
3825
- entityTouched = entityTouched || optionAxes.value.changed || variantIds.value.changed || media.value.changed || attributes.value.changed;
3860
+ entityTouched = entityTouched || optionAxes.value.changed || variantIds.value.changed || taxonomy.value.changed || media.value.changed || attributes.value.changed;
3826
3861
  if (entityTouched) entitiesTouched += 1;
3827
3862
  // Counted from what was written, not from the stored hash: a hash that moved while the
3828
3863
  // product did not (a blanked map row, a change in how an item serialises) is not drift.