@porulle/plugin-channel-connector 0.53.1 → 0.55.0
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/deletion-policy.d.ts +19 -0
- package/dist/deletion-policy.js +28 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/service.d.ts +13 -0
- package/dist/service.js +71 -51
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/deletion-policy.ts +30 -0
- package/src/index.ts +2 -0
- package/src/service.ts +87 -59
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@porulle/plugin-channel-connector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@hono/zod-openapi": "^1.2.2",
|
|
21
21
|
"hono": "^4.12.5",
|
|
22
|
-
"@porulle/core": "0.
|
|
22
|
+
"@porulle/core": "0.55.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^24.5.2",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE deletion policy for a store's mapped products that a fetch no longer lists — one home for
|
|
3
|
+
* the rule, shared by `reconcile()` and the app's import finalize barrier.
|
|
4
|
+
*
|
|
5
|
+
* A fetch that succeeded is not a fetch that was complete: a merchant API hiccup, pagination that
|
|
6
|
+
* stopped early, or an auth scope change returns fewer products than the store has, and archiving
|
|
7
|
+
* "everything absent" then wipes the store. Found on the sim, 2026-09-24. So the plan REFUSES:
|
|
8
|
+
* - an empty fetch over a store that has mapped products;
|
|
9
|
+
* - more absent products than max(ABSENT_ARCHIVE_FLOOR, floor(ABSENT_ARCHIVE_FRACTION × mapped)).
|
|
10
|
+
* A refusal archives nothing; the caller surfaces the reason for a person to look at.
|
|
11
|
+
*/
|
|
12
|
+
export const ABSENT_ARCHIVE_FLOOR = 5;
|
|
13
|
+
export const ABSENT_ARCHIVE_FRACTION = 0.2;
|
|
14
|
+
|
|
15
|
+
export type AbsentArchivePlan = { archive: string[] } | { refused: string };
|
|
16
|
+
|
|
17
|
+
export function planAbsentArchives(mappedExternalIds: Iterable<string>, presentExternalIds: Iterable<string>): AbsentArchivePlan {
|
|
18
|
+
const mapped = [...new Set(mappedExternalIds)];
|
|
19
|
+
const present = new Set(presentExternalIds);
|
|
20
|
+
const absent = mapped.filter((externalId) => !present.has(externalId));
|
|
21
|
+
if (absent.length === 0) return { archive: [] };
|
|
22
|
+
if (present.size === 0) {
|
|
23
|
+
return { refused: `The fetch listed no products while ${mapped.length} are mapped; refusing to archive them.` };
|
|
24
|
+
}
|
|
25
|
+
const bound = Math.max(ABSENT_ARCHIVE_FLOOR, Math.floor(ABSENT_ARCHIVE_FRACTION * mapped.length));
|
|
26
|
+
if (absent.length > bound) {
|
|
27
|
+
return { refused: `${absent.length} of ${mapped.length} mapped products are absent from the fetch, above the bound of ${bound}; refusing to archive them.` };
|
|
28
|
+
}
|
|
29
|
+
return { archive: absent };
|
|
30
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -53,6 +53,8 @@ type ChannelRouteContext = {
|
|
|
53
53
|
raw?: unknown;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
export { ABSENT_ARCHIVE_FLOOR, ABSENT_ARCHIVE_FRACTION, planAbsentArchives } from "./deletion-policy.js";
|
|
57
|
+
export type { AbsentArchivePlan } from "./deletion-policy.js";
|
|
56
58
|
export { mockChannelConnector } from "./mock-connector.js";
|
|
57
59
|
export type { MockChannelConnectorOptions } from "./mock-connector.js";
|
|
58
60
|
export {
|
package/src/service.ts
CHANGED
|
@@ -6,9 +6,12 @@ import {
|
|
|
6
6
|
PluginErr,
|
|
7
7
|
createTxContext,
|
|
8
8
|
createSystemActor,
|
|
9
|
+
linkFieldPaths,
|
|
10
|
+
writeEntityLinks,
|
|
9
11
|
} from "@porulle/core";
|
|
10
12
|
import type {
|
|
11
13
|
Actor,
|
|
14
|
+
EntityLinkRows,
|
|
12
15
|
ChannelCatalogItem,
|
|
13
16
|
ChannelConnector,
|
|
14
17
|
ChannelOrderSlice,
|
|
@@ -40,9 +43,6 @@ import {
|
|
|
40
43
|
customerAddresses,
|
|
41
44
|
customers,
|
|
42
45
|
entityMedia,
|
|
43
|
-
entityBrands,
|
|
44
|
-
entityCategories,
|
|
45
|
-
entityTags,
|
|
46
46
|
inventoryLevels,
|
|
47
47
|
mediaAssets,
|
|
48
48
|
optionTypes,
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
variantOptionValues,
|
|
61
61
|
} from "@porulle/core/schema";
|
|
62
62
|
import type { SellableEntityRevisionSnapshot } from "@porulle/core/schema";
|
|
63
|
+
import { planAbsentArchives } from "./deletion-policy.js";
|
|
63
64
|
import {
|
|
64
65
|
channelCatalogPushEvents,
|
|
65
66
|
channelCatalogPushes,
|
|
@@ -164,6 +165,8 @@ export interface ReconcileReport extends Record<string, unknown> {
|
|
|
164
165
|
inventoryUpdated: number;
|
|
165
166
|
openConflicts: number;
|
|
166
167
|
driftAlert: boolean;
|
|
168
|
+
/** Why this reconcile archived nothing although mapped products were absent (`planAbsentArchives`). */
|
|
169
|
+
refused?: string;
|
|
167
170
|
skipped?: CatalogFieldSkip[];
|
|
168
171
|
conflicts?: CatalogFieldConflict[];
|
|
169
172
|
warnings?: string[];
|
|
@@ -517,6 +520,8 @@ interface CatalogService {
|
|
|
517
520
|
actor: Actor,
|
|
518
521
|
ctx?: CatalogWriteContext,
|
|
519
522
|
): Promise<{ ok: true; value: undefined } | { ok: false; error: { message: string } }>;
|
|
523
|
+
/** Moves the entity's updated_at and fires catalog.afterUpdate for a change to related rows. */
|
|
524
|
+
notifyEntityChanged(entityId: string, changedFieldPaths: readonly string[], actor: Actor | null, ctx?: TxContext<PluginDb>): Promise<void>;
|
|
520
525
|
recordEntityRevision(
|
|
521
526
|
entityId: string,
|
|
522
527
|
actor: Actor,
|
|
@@ -581,16 +586,6 @@ interface MediaService {
|
|
|
581
586
|
},
|
|
582
587
|
actor: Actor,
|
|
583
588
|
): Promise<ServiceResult<{ id: string; url: string }>>;
|
|
584
|
-
attachToEntity(
|
|
585
|
-
input: {
|
|
586
|
-
entityId: string;
|
|
587
|
-
mediaAssetId: string;
|
|
588
|
-
role: "primary" | "gallery" | "thumbnail" | "video" | "document";
|
|
589
|
-
variantId?: string;
|
|
590
|
-
sortOrder?: number;
|
|
591
|
-
},
|
|
592
|
-
actor: Actor,
|
|
593
|
-
): Promise<ServiceResult<undefined>>;
|
|
594
589
|
listEntityMedia(
|
|
595
590
|
entityId: string,
|
|
596
591
|
opts?: { variantId?: string; orgId?: string },
|
|
@@ -1127,6 +1122,9 @@ function toImportProduct(item: ChannelCatalogItem): ImportProduct {
|
|
|
1127
1122
|
};
|
|
1128
1123
|
}
|
|
1129
1124
|
|
|
1125
|
+
/** One item's link writes, planned before the transaction that commits them (`commitEntityLinks`). */
|
|
1126
|
+
type PlannedLinks = { [K in keyof EntityLinkRows]-?: Array<NonNullable<EntityLinkRows[K]>[number]> };
|
|
1127
|
+
|
|
1130
1128
|
export class ChannelConnectorService {
|
|
1131
1129
|
private readonly connectors = new Map<string, ChannelConnector>();
|
|
1132
1130
|
private readonly transact: PluginTxFn;
|
|
@@ -2050,15 +2048,12 @@ export class ChannelConnectorService {
|
|
|
2050
2048
|
item: ChannelCatalogItem,
|
|
2051
2049
|
actor: Actor,
|
|
2052
2050
|
warnings: string[],
|
|
2053
|
-
): Promise<PluginResult<
|
|
2051
|
+
): Promise<PluginResult<Pick<PlannedLinks, "categories" | "brands" | "tags">>> {
|
|
2052
|
+
// Resolves (creating where missing) the category, brand and tag rows the item names, and PLANS
|
|
2053
|
+
// the entity's links to them. The links are written by `commitEntityLinks`, in one transaction
|
|
2054
|
+
// with the entity's version bump; a link that already exists writes nothing there.
|
|
2054
2055
|
const taxonomy = await this.taxonomyFor(orgId);
|
|
2055
|
-
|
|
2056
|
-
// there is not re-written, and a link that is added is what reports the taxonomy as changed.
|
|
2057
|
-
const linkedCategories = new Set((item.categories ?? []).length === 0 ? [] : (await this.db
|
|
2058
|
-
.select({ id: entityCategories.categoryId }).from(entityCategories).where(eq(entityCategories.entityId, entityId))).map((row) => row.id));
|
|
2059
|
-
const linkedBrands = new Set(!item.brand ? [] : (await this.db
|
|
2060
|
-
.select({ id: entityBrands.brandId }).from(entityBrands).where(eq(entityBrands.entityId, entityId))).map((row) => row.id));
|
|
2061
|
-
let changed = false;
|
|
2056
|
+
const links: Pick<PlannedLinks, "categories" | "brands" | "tags"> = { categories: [], brands: [], tags: [] };
|
|
2062
2057
|
const categoryRows = taxonomy.categories;
|
|
2063
2058
|
for (const slug of new Set(item.categories ?? [])) {
|
|
2064
2059
|
let category = categoryRows.find((row) => row.slug === slug);
|
|
@@ -2077,10 +2072,7 @@ export class ChannelConnectorService {
|
|
|
2077
2072
|
category = created.value;
|
|
2078
2073
|
categoryRows.push(category);
|
|
2079
2074
|
}
|
|
2080
|
-
|
|
2081
|
-
const linked = await this.catalog.addToCategory(entityId, category.id, actor);
|
|
2082
|
-
if (!linked.ok) return PluginErr(linked.error.message);
|
|
2083
|
-
changed = true;
|
|
2075
|
+
links.categories.push({ entityId, categoryId: category.id, sortOrder: 0 });
|
|
2084
2076
|
}
|
|
2085
2077
|
|
|
2086
2078
|
const brandRows = taxonomy.brands;
|
|
@@ -2098,11 +2090,7 @@ export class ChannelConnectorService {
|
|
|
2098
2090
|
brand = created.value;
|
|
2099
2091
|
brandRows.push(brand);
|
|
2100
2092
|
}
|
|
2101
|
-
|
|
2102
|
-
const linked = await this.catalog.addToBrand(entityId, brand.id, actor);
|
|
2103
|
-
if (!linked.ok) return PluginErr(linked.error.message);
|
|
2104
|
-
changed = true;
|
|
2105
|
-
}
|
|
2093
|
+
links.brands.push({ entityId, brandId: brand.id, sortOrder: 0 });
|
|
2106
2094
|
}
|
|
2107
2095
|
|
|
2108
2096
|
const tagRows = taxonomy.tags;
|
|
@@ -2117,10 +2105,46 @@ export class ChannelConnectorService {
|
|
|
2117
2105
|
if (!tag) return PluginErr(`Tag "${slug}" was not persisted.`);
|
|
2118
2106
|
tagRows.push(tag);
|
|
2119
2107
|
}
|
|
2120
|
-
|
|
2121
|
-
|
|
2108
|
+
links.tags.push({ entityId, tagId: tag.id });
|
|
2109
|
+
}
|
|
2110
|
+
return Ok(links);
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
/**
|
|
2114
|
+
* Writes one item's planned links and versions the entity for them, in ONE transaction: the
|
|
2115
|
+
* entity's `updated_at` moves with the links or not at all, and `catalog.afterUpdate` fires once
|
|
2116
|
+
* for the item with every link path that really changed (`["categories","tags"]`), not once per
|
|
2117
|
+
* link. Returns those paths; empty means nothing changed.
|
|
2118
|
+
*
|
|
2119
|
+
* An entity CREATED by this converge is not versioned for its links: its creation already put it
|
|
2120
|
+
* in front of every consumer, so a bump here would re-project a product in the same breath as its
|
|
2121
|
+
* first projection — the cold-import cost this rule exists to avoid.
|
|
2122
|
+
*/
|
|
2123
|
+
private async commitEntityLinks(
|
|
2124
|
+
orgId: string,
|
|
2125
|
+
entityId: string,
|
|
2126
|
+
planned: PlannedLinks,
|
|
2127
|
+
previousRoles: Map<string, string>,
|
|
2128
|
+
isNew: boolean,
|
|
2129
|
+
actor: Actor,
|
|
2130
|
+
): Promise<PluginResult<string[]>> {
|
|
2131
|
+
try {
|
|
2132
|
+
return Ok(await this.transact(async (tx) => {
|
|
2133
|
+
const written = await writeEntityLinks(tx, orgId, planned);
|
|
2134
|
+
const paths = new Set(linkFieldPaths(written).get(entityId));
|
|
2135
|
+
for (const row of written.placed) {
|
|
2136
|
+
const previous = previousRoles.get(`${row.mediaAssetId}:${row.variantId}`);
|
|
2137
|
+
if (previous !== undefined) paths.add(`media.${previous}`);
|
|
2138
|
+
}
|
|
2139
|
+
const changed = [...paths].sort();
|
|
2140
|
+
if (changed.length > 0 && !isNew) {
|
|
2141
|
+
await this.catalog.notifyEntityChanged(entityId, changed, actor, createTxContext(tx, { actor }));
|
|
2142
|
+
}
|
|
2143
|
+
return changed;
|
|
2144
|
+
}));
|
|
2145
|
+
} catch (error) {
|
|
2146
|
+
return PluginErr(error instanceof Error ? error.message : "Failed to write the entity's links.");
|
|
2122
2147
|
}
|
|
2123
|
-
return Ok({ changed });
|
|
2124
2148
|
}
|
|
2125
2149
|
|
|
2126
2150
|
private async applyMedia(
|
|
@@ -2131,7 +2155,15 @@ export class ChannelConnectorService {
|
|
|
2131
2155
|
actor: Actor,
|
|
2132
2156
|
warnings: string[],
|
|
2133
2157
|
owners: Map<FieldPath, FieldOwner>,
|
|
2134
|
-
): Promise<PluginResult<{
|
|
2158
|
+
): Promise<PluginResult<{
|
|
2159
|
+
imported: number;
|
|
2160
|
+
uploaded: boolean;
|
|
2161
|
+
skipped: FieldPath[];
|
|
2162
|
+
links: Pick<PlannedLinks, "media" | "mediaPlacements">;
|
|
2163
|
+
/** The role a re-placed link held before, keyed `${mediaAssetId}:${variantId}` — its path changes too. */
|
|
2164
|
+
previousRoles: Map<string, string>;
|
|
2165
|
+
}>> {
|
|
2166
|
+
// Uploads what is missing and PLANS the entity's media links; `commitEntityLinks` writes them.
|
|
2135
2167
|
const images = item.images ?? [];
|
|
2136
2168
|
const externalIds = [...new Set(images.map((image) => image.externalId).filter((id): id is string => id != null))];
|
|
2137
2169
|
const urlHashes = [...new Set(images.map((image) => hash(image.url)))];
|
|
@@ -2150,8 +2182,10 @@ export class ChannelConnectorService {
|
|
|
2150
2182
|
));
|
|
2151
2183
|
const links = await this.db.select().from(entityMedia).where(eq(entityMedia.entityId, entityId));
|
|
2152
2184
|
let imported = 0;
|
|
2153
|
-
let
|
|
2185
|
+
let uploaded = false;
|
|
2154
2186
|
const skipped: FieldPath[] = [];
|
|
2187
|
+
const planned: Pick<PlannedLinks, "media" | "mediaPlacements"> = { media: [], mediaPlacements: [] };
|
|
2188
|
+
const previousRoles = new Map<string, string>();
|
|
2155
2189
|
|
|
2156
2190
|
// A Cloudflare Worker may hold at most six simultaneous outbound connections per
|
|
2157
2191
|
// invocation, and one image costs two of them — the download and the storage put — so
|
|
@@ -2273,7 +2307,7 @@ export class ChannelConnectorService {
|
|
|
2273
2307
|
for (const resolved of resolvedImages) {
|
|
2274
2308
|
warnings.push(...resolved.imageWarnings);
|
|
2275
2309
|
imported += resolved.imported;
|
|
2276
|
-
if (resolved.imageChanged)
|
|
2310
|
+
if (resolved.imageChanged) uploaded = true;
|
|
2277
2311
|
}
|
|
2278
2312
|
|
|
2279
2313
|
for (const [imageIndex, image] of images.entries()) {
|
|
@@ -2302,24 +2336,12 @@ export class ChannelConnectorService {
|
|
|
2302
2336
|
if (skipped.includes(currentRolePath) || skipped.includes(incomingRolePath)) continue;
|
|
2303
2337
|
}
|
|
2304
2338
|
if (existingLink.role !== image.role || existingLink.sortOrder !== (image.sortOrder ?? 0)) {
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
eq(entityMedia.mediaAssetId, mediaAssetId),
|
|
2308
|
-
target.variantId === undefined ? isNull(entityMedia.variantId) : eq(entityMedia.variantId, target.variantId),
|
|
2309
|
-
));
|
|
2310
|
-
changed = true;
|
|
2339
|
+
planned.mediaPlacements.push({ entityId, variantId: target.variantId ?? null, mediaAssetId, role: image.role, sortOrder: image.sortOrder ?? 0 });
|
|
2340
|
+
previousRoles.set(`${mediaAssetId}:${target.variantId ?? null}`, existingLink.role);
|
|
2311
2341
|
}
|
|
2312
2342
|
continue;
|
|
2313
2343
|
}
|
|
2314
|
-
|
|
2315
|
-
entityId,
|
|
2316
|
-
mediaAssetId,
|
|
2317
|
-
role: image.role,
|
|
2318
|
-
sortOrder: image.sortOrder ?? 0,
|
|
2319
|
-
...(target.variantId !== undefined ? { variantId: target.variantId } : {}),
|
|
2320
|
-
}, actor);
|
|
2321
|
-
if (!attached.ok) return PluginErr(attached.error.message);
|
|
2322
|
-
changed = true;
|
|
2344
|
+
planned.media.push({ entityId, variantId: target.variantId ?? null, mediaAssetId, role: image.role, sortOrder: image.sortOrder ?? 0 });
|
|
2323
2345
|
links.push({
|
|
2324
2346
|
entityId,
|
|
2325
2347
|
mediaAssetId,
|
|
@@ -2330,7 +2352,7 @@ export class ChannelConnectorService {
|
|
|
2330
2352
|
});
|
|
2331
2353
|
}
|
|
2332
2354
|
}
|
|
2333
|
-
return Ok({ imported,
|
|
2355
|
+
return Ok({ imported, uploaded, skipped, links: planned, previousRoles });
|
|
2334
2356
|
}
|
|
2335
2357
|
|
|
2336
2358
|
private async getStoreRecord(orgId: string, id: string): Promise<ConnectedStore | undefined> {
|
|
@@ -3328,13 +3350,13 @@ export class ChannelConnectorService {
|
|
|
3328
3350
|
}));
|
|
3329
3351
|
|
|
3330
3352
|
if (outcomes.length > 0) {
|
|
3331
|
-
await this.db
|
|
3332
|
-
{ entityId, mediaAssetId, role: "primary" as const, sortOrder: hero.sortOrder ?? 0 },
|
|
3353
|
+
await writeEntityLinks(this.db, orgId, { media: outcomes.flatMap(({ entityId, mediaAssetId, hero, variantIds }) => [
|
|
3354
|
+
{ entityId, variantId: null, mediaAssetId, role: "primary" as const, sortOrder: hero.sortOrder ?? 0 },
|
|
3333
3355
|
...(hero.variantExternalIds ?? []).flatMap((externalId) => {
|
|
3334
3356
|
const variantId = variantIds[externalId];
|
|
3335
3357
|
return variantId === undefined ? [] : [{ entityId, variantId, mediaAssetId, role: hero.role, sortOrder: hero.sortOrder ?? 0 }];
|
|
3336
3358
|
}),
|
|
3337
|
-
]))
|
|
3359
|
+
]) });
|
|
3338
3360
|
}
|
|
3339
3361
|
return { heroesImported: outcomes.filter((outcome) => outcome.imported).length, mediaFailures, deferredMedia };
|
|
3340
3362
|
}
|
|
@@ -4073,11 +4095,13 @@ export class ChannelConnectorService {
|
|
|
4073
4095
|
if (!taxonomy.ok) { failures.push({ externalId: item.externalId, error: taxonomy.error }); continue; }
|
|
4074
4096
|
const media = await this.applyMedia(orgId, entityId, writable, variantIds.value.value, actor, warnings, owners);
|
|
4075
4097
|
if (!media.ok) return media;
|
|
4098
|
+
const links = await this.commitEntityLinks(orgId, entityId, { ...taxonomy.value, ...media.value.links }, media.value.previousRoles, isNew, actor);
|
|
4099
|
+
if (!links.ok) { failures.push({ externalId: item.externalId, error: links.error }); continue; }
|
|
4076
4100
|
attributesCreated += attributes.value.created;
|
|
4077
4101
|
mediaImported += media.value.imported;
|
|
4078
4102
|
variantsGivenOptionValues += variantIds.value.repaired;
|
|
4079
4103
|
skipped.push(...media.value.skipped.map((fieldPath) => ({ entityId, fieldPath })));
|
|
4080
|
-
entityTouched = entityTouched || optionAxes.value.changed || variantIds.value.changed ||
|
|
4104
|
+
entityTouched = entityTouched || optionAxes.value.changed || variantIds.value.changed || links.value.length > 0 || media.value.uploaded || attributes.value.changed
|
|
4081
4105
|
|| identity.written.has(item.externalId);
|
|
4082
4106
|
const skuClashes = identity.clashes.get(item.externalId);
|
|
4083
4107
|
if (skuClashes) {
|
|
@@ -4207,11 +4231,14 @@ export class ChannelConnectorService {
|
|
|
4207
4231
|
|
|
4208
4232
|
const converged = await this.convergeCatalogItems(orgId, storeId, items, actor);
|
|
4209
4233
|
if (!converged.ok) return converged;
|
|
4210
|
-
|
|
4234
|
+
// Planned BEFORE anything is archived: an empty or truncated fetch is refused whole (see
|
|
4235
|
+
// `planAbsentArchives`), and the refusal is reported, never half-applied.
|
|
4236
|
+
const plan = planAbsentArchives(entityMappings.map((mapping) => mapping.externalId), items.map((item) => item.externalId));
|
|
4237
|
+
const toArchive = new Set("archive" in plan ? plan.archive : []);
|
|
4211
4238
|
let archived = 0;
|
|
4212
4239
|
const skipped = [...converged.value.skipped];
|
|
4213
4240
|
for (const mapping of entityMappings) {
|
|
4214
|
-
if (
|
|
4241
|
+
if (!toArchive.has(mapping.externalId)) continue;
|
|
4215
4242
|
const [entity] = await this.db.select({ status: sellableEntities.status }).from(sellableEntities).where(and(
|
|
4216
4243
|
eq(sellableEntities.organizationId, orgId),
|
|
4217
4244
|
eq(sellableEntities.id, mapping.entityId),
|
|
@@ -4263,7 +4290,8 @@ export class ChannelConnectorService {
|
|
|
4263
4290
|
archived,
|
|
4264
4291
|
inventoryUpdated,
|
|
4265
4292
|
openConflicts: openConflictRows.length,
|
|
4266
|
-
driftAlert: converged.value.imported + converged.value.converged + archived > threshold,
|
|
4293
|
+
driftAlert: "refused" in plan || converged.value.imported + converged.value.converged + archived > threshold,
|
|
4294
|
+
...("refused" in plan ? { refused: plan.refused } : {}),
|
|
4267
4295
|
...(skipped.length > 0 ? { skipped: uniqueSkipped(skipped) } : {}),
|
|
4268
4296
|
...(converged.value.conflicts.length > 0 ? { conflicts: converged.value.conflicts } : {}),
|
|
4269
4297
|
...(converged.value.warnings.length > 0 ? { warnings: converged.value.warnings } : {}),
|