@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/dist/service.js CHANGED
@@ -3,7 +3,7 @@ import { CommerceInvalidTransitionError, CommerceValidationError, Ok, PluginErr,
3
3
  import { isValidFieldPath, requireUserId } from "@porulle/core";
4
4
  import { CHANNEL_CONVERGENCE_CTX } from "./catalog-push-trigger.js";
5
5
  import { and, desc, eq, inArray, isNull, lte, or, sql } from "@porulle/core/drizzle";
6
- import { brands, categories, customerAddresses, customers, entityMedia, entityTags, inventoryLevels, mediaAssets, optionTypes, optionValues, orderLineItems, orders, prices, sellableAttributes, sellableCustomFields, sellableEntities, sellableEntityRevisions, entityFieldDefinitions, tags, variants, variantOptionValues, } from "@porulle/core/schema";
6
+ import { brands, categories, customerAddresses, customers, entityMedia, entityBrands, entityCategories, entityTags, inventoryLevels, mediaAssets, optionTypes, optionValues, orderLineItems, orders, prices, sellableAttributes, sellableCustomFields, sellableEntities, sellableEntityRevisions, entityFieldDefinitions, tags, variants, variantOptionValues, } from "@porulle/core/schema";
7
7
  import { channelCatalogPushEvents, channelCatalogPushes, channelCatalogConflicts, channelCatalogConflictEvents, channelEntityMap, channelExportEvents, channelOrderExports, connectedStores, channelRefundEvents, channelRefundRequests, } from "./schema.js";
8
8
  import { mergeCatalogFieldMapping, normalizeCatalogFieldMapping, selectCatalogFieldMapping, } from "./catalog-field-mapping.js";
9
9
  export const CATALOG_PUSH_BATCH_SIZES = {
@@ -973,6 +973,14 @@ export class ChannelConnectorService {
973
973
  existingOptionValues.set(row.variantId, list);
974
974
  }
975
975
  }
976
+ // The product's plain base prices, read once. `setBasePrice` upserts and fires its hook even at
977
+ // the same amount, so an unchanged price is skipped here — and a price that IS written is what
978
+ // makes the variant "changed", which is what `converged` counts.
979
+ const storedPrices = item.variants.some((variant) => (variant.prices ?? []).length > 0)
980
+ ? await this.db.select({ variantId: prices.variantId, currency: prices.currency, amount: prices.amount, compareAtAmount: prices.compareAtAmount })
981
+ .from(prices)
982
+ .where(and(eq(prices.organizationId, orgId), eq(prices.entityId, entityId), isNull(prices.customerGroupId), isNull(prices.minQuantity), isNull(prices.maxQuantity), isNull(prices.validFrom), isNull(prices.validUntil)))
983
+ : [];
976
984
  for (const sourceVariant of item.variants) {
977
985
  const fullSourceVariant = fullItem.variants.find((variant) => variant.externalId === sourceVariant.externalId) ?? sourceVariant;
978
986
  let mapping = mappings.find((row) => row.externalId === sourceVariant.externalId);
@@ -1081,6 +1089,10 @@ export class ChannelConnectorService {
1081
1089
  }
1082
1090
  }
1083
1091
  for (const price of sourceVariant.prices ?? []) {
1092
+ const currency = price.currency.trim().toUpperCase();
1093
+ const stored = storedPrices.find((row) => row.variantId === variantId && row.currency === currency);
1094
+ if (stored && stored.amount === price.amount && stored.compareAtAmount === (price.compareAtAmount ?? null))
1095
+ continue;
1084
1096
  const priced = await this.pricing.setBasePrice({
1085
1097
  entityId,
1086
1098
  variantId,
@@ -1090,6 +1102,7 @@ export class ChannelConnectorService {
1090
1102
  }, actor);
1091
1103
  if (!priced.ok)
1092
1104
  return PluginErr(priced.error.message);
1105
+ changed = true;
1093
1106
  }
1094
1107
  // Was UNCONDITIONAL: one UPDATE per variant on every import, including a re-sync where the
1095
1108
  // variant is byte-identical. `mapping.syncHash` is already in hand from the select above, so
@@ -1141,6 +1154,13 @@ export class ChannelConnectorService {
1141
1154
  }
1142
1155
  async applyTaxonomy(orgId, entityId, item, actor, warnings) {
1143
1156
  const taxonomy = await this.taxonomyFor(orgId);
1157
+ // The links this entity already has, read only for the classes the item names. A link already
1158
+ // there is not re-written, and a link that is added is what reports the taxonomy as changed.
1159
+ const linkedCategories = new Set((item.categories ?? []).length === 0 ? [] : (await this.db
1160
+ .select({ id: entityCategories.categoryId }).from(entityCategories).where(eq(entityCategories.entityId, entityId))).map((row) => row.id));
1161
+ const linkedBrands = new Set(!item.brand ? [] : (await this.db
1162
+ .select({ id: entityBrands.brandId }).from(entityBrands).where(eq(entityBrands.entityId, entityId))).map((row) => row.id));
1163
+ let changed = false;
1144
1164
  const categoryRows = taxonomy.categories;
1145
1165
  for (const slug of new Set(item.categories ?? [])) {
1146
1166
  let category = categoryRows.find((row) => row.slug === slug);
@@ -1158,9 +1178,12 @@ export class ChannelConnectorService {
1158
1178
  category = createdCategory;
1159
1179
  categoryRows.push(category);
1160
1180
  }
1181
+ if (linkedCategories.has(category.id))
1182
+ continue;
1161
1183
  const linked = await this.catalog.addToCategory(entityId, category.id, actor);
1162
1184
  if (!linked.ok)
1163
1185
  return PluginErr(linked.error.message);
1186
+ changed = true;
1164
1187
  }
1165
1188
  const brandRows = taxonomy.brands;
1166
1189
  if (item.brand) {
@@ -1175,9 +1198,12 @@ export class ChannelConnectorService {
1175
1198
  brand = createdBrand;
1176
1199
  brandRows.push(brand);
1177
1200
  }
1178
- const linked = await this.catalog.addToBrand(entityId, brand.id, actor);
1179
- if (!linked.ok)
1180
- return PluginErr(linked.error.message);
1201
+ if (!linkedBrands.has(brand.id)) {
1202
+ const linked = await this.catalog.addToBrand(entityId, brand.id, actor);
1203
+ if (!linked.ok)
1204
+ return PluginErr(linked.error.message);
1205
+ changed = true;
1206
+ }
1181
1207
  }
1182
1208
  const tagRows = taxonomy.tags;
1183
1209
  for (const slug of new Set(item.tags ?? [])) {
@@ -1189,9 +1215,11 @@ export class ChannelConnectorService {
1189
1215
  return PluginErr(`Tag "${slug}" was not persisted.`);
1190
1216
  tagRows.push(tag);
1191
1217
  }
1192
- await this.db.insert(entityTags).values({ entityId, tagId: tag.id }).onConflictDoNothing();
1218
+ const added = await this.db.insert(entityTags).values({ entityId, tagId: tag.id }).onConflictDoNothing().returning({ tagId: entityTags.tagId });
1219
+ if (added.length > 0)
1220
+ changed = true;
1193
1221
  }
1194
- return Ok(undefined);
1222
+ return Ok({ changed });
1195
1223
  }
1196
1224
  async applyMedia(orgId, entityId, item, variantIds, actor, warnings, owners) {
1197
1225
  const images = item.images ?? [];
@@ -2886,7 +2914,7 @@ export class ChannelConnectorService {
2886
2914
  mediaImported += media.value.imported;
2887
2915
  variantsGivenOptionValues += variantIds.value.repaired;
2888
2916
  skipped.push(...media.value.skipped.map((fieldPath) => ({ entityId, fieldPath })));
2889
- entityTouched = entityTouched || optionAxes.value.changed || variantIds.value.changed || media.value.changed || attributes.value.changed;
2917
+ entityTouched = entityTouched || optionAxes.value.changed || variantIds.value.changed || taxonomy.value.changed || media.value.changed || attributes.value.changed;
2890
2918
  if (entityTouched)
2891
2919
  entitiesTouched += 1;
2892
2920
  // Counted from what was written, not from the stored hash: a hash that moved while the