mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 12:45:24 +02:00
feat(bc): sync 40hc pricing
This commit is contained in:
+109
-8
@@ -62,6 +62,11 @@ export function buildBusinessCentralMappingPreview(headers, row) {
|
||||
const cpnpIdx = findHeaderIndex(headers, ['cpnp']);
|
||||
|
||||
const unitsOuterIdx = findHeaderIndex(headers, ['units', 'outer']);
|
||||
const units40HqIdx = (() => {
|
||||
const hqIdx = findHeaderIndex(headers, ['40', 'hq']);
|
||||
if (hqIdx >= 0) return hqIdx;
|
||||
return findHeaderIndex(headers, ['40', 'hc']);
|
||||
})();
|
||||
const innerWIdx = findHeaderIndex(headers, ['inner', 'w']);
|
||||
const innerLIdx = findHeaderIndex(headers, ['inner', 'l']);
|
||||
const innerHIdx = findHeaderIndex(headers, ['inner', 'h']);
|
||||
@@ -92,10 +97,17 @@ export function buildBusinessCentralMappingPreview(headers, row) {
|
||||
height: toBcDecimal(getValue(row, outerHIdx)),
|
||||
};
|
||||
|
||||
const itemUnits40HCPayload = {
|
||||
itemNo: getValue(row, articleNoIdx),
|
||||
code: '40HC',
|
||||
qtyPerUnitOfMeasure: toBcDecimal(getValue(row, units40HqIdx)),
|
||||
};
|
||||
|
||||
return {
|
||||
articleNo,
|
||||
itemsPayload,
|
||||
itemUnitsOfMeasurePayload,
|
||||
itemUnits40HCPayload,
|
||||
itemsFields: [
|
||||
makeFieldPreview('Article No.', articleNoIdx, 'no', itemsPayload.no),
|
||||
makeFieldPreview('Article Details - English', articleDetailsEnIdx, 'articleDetailsEnglish', itemsPayload.articleDetailsEnglish),
|
||||
@@ -114,6 +126,10 @@ export function buildBusinessCentralMappingPreview(headers, row) {
|
||||
makeFieldPreview('Outer L (cm)', outerLIdx, 'length', itemUnitsOfMeasurePayload.length),
|
||||
makeFieldPreview('Outer H (cm)', outerHIdx, 'height', itemUnitsOfMeasurePayload.height),
|
||||
],
|
||||
itemUnits40HCFields: [
|
||||
makeFieldPreview('Article No.', articleNoIdx, 'itemNo', itemUnits40HCPayload.itemNo),
|
||||
makeFieldPreview('Units 40FT HQ', units40HqIdx, 'qtyPerUnitOfMeasure', itemUnits40HCPayload.qtyPerUnitOfMeasure),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -269,16 +285,26 @@ export async function fetchBusinessCentralSnapshot(config, token, articleNo) {
|
||||
const { json: itemsJson, etag: itemsEtag } = await fetchJsonOrThrow(itemsUrl, token, 'BC GET items');
|
||||
const item = Array.isArray(itemsJson.value) && itemsJson.value.length > 0 ? itemsJson.value[0] : null;
|
||||
|
||||
const uomFilter = encodeURIComponent(`itemNo eq '${articleNo}' and code eq '${config.itemUnitsCode || 'OUTER'}'`);
|
||||
const uomUrl = `${getItemUnitsOfMeasureUrl(config)}?$filter=${uomFilter}&$select=${getItemUnitsSelect()}`;
|
||||
const { json: uomJson, etag: uomEtag } = await fetchJsonOrThrow(uomUrl, token, 'BC GET itemUnitsOfMeasure');
|
||||
const itemUnitsOfMeasure = Array.isArray(uomJson.value) && uomJson.value.length > 0 ? uomJson.value[0] : null;
|
||||
const outerCode = config.itemUnitsCode || 'OUTER';
|
||||
const uomUrl = getItemUnitsOfMeasureUrl(config);
|
||||
|
||||
const outerFilter = encodeURIComponent(`itemNo eq '${articleNo}' and code eq '${outerCode}'`);
|
||||
const outerUrl = `${uomUrl}?$filter=${outerFilter}&$select=${getItemUnitsSelect()}`;
|
||||
const { json: outerJson, etag: outerEtag } = await fetchJsonOrThrow(outerUrl, token, 'BC GET itemUnitsOfMeasure OUTER');
|
||||
const itemUnitsOfMeasure = Array.isArray(outerJson.value) && outerJson.value.length > 0 ? outerJson.value[0] : null;
|
||||
|
||||
const hcFilter = encodeURIComponent(`itemNo eq '${articleNo}' and code eq '40HC'`);
|
||||
const hcUrl = `${uomUrl}?$filter=${hcFilter}&$select=${getItemUnitsSelect()}`;
|
||||
const { json: hcJson, etag: hcEtag } = await fetchJsonOrThrow(hcUrl, token, 'BC GET itemUnitsOfMeasure 40HC');
|
||||
const itemUnits40HC = Array.isArray(hcJson.value) && hcJson.value.length > 0 ? hcJson.value[0] : null;
|
||||
|
||||
return {
|
||||
item,
|
||||
itemUnitsOfMeasure,
|
||||
itemUnits40HC,
|
||||
itemEtag: item?.['@odata.etag'] || itemsEtag || '*',
|
||||
itemUnitsEtag: itemUnitsOfMeasure?.['@odata.etag'] || uomEtag || '*',
|
||||
itemUnitsEtag: itemUnitsOfMeasure?.['@odata.etag'] || outerEtag || '*',
|
||||
itemUnits40HCEtag: itemUnits40HC?.['@odata.etag'] || hcEtag || '*',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -331,19 +357,34 @@ export function buildBusinessCentralSyncPreview(config, mapping, snapshot) {
|
||||
writeBodyTemplate: config.itemUnitsWriteBodyTemplate || null,
|
||||
});
|
||||
|
||||
const itemUnits40HCSection = makePreviewSection({
|
||||
type: 'itemUnits40HC',
|
||||
desiredPayload: mapping.itemUnits40HCPayload,
|
||||
currentRecord: snapshot.itemUnits40HC,
|
||||
fieldPreviews: mapping.itemUnits40HCFields,
|
||||
writeMethod: config.itemUnits40HCWriteMethod || config.itemUnitsWriteMethod || null,
|
||||
writeUrlTemplate: config.itemUnits40HCWriteUrlTemplate || config.itemUnitsWriteUrlTemplate || null,
|
||||
writeBodyTemplate: config.itemUnits40HCWriteBodyTemplate || config.itemUnitsWriteBodyTemplate || null,
|
||||
});
|
||||
|
||||
const previewPayload = {
|
||||
articleNo: mapping.articleNo,
|
||||
items: itemsSection,
|
||||
itemUnitsOfMeasure: itemUnitsSection,
|
||||
itemUnits40HC: itemUnits40HCSection,
|
||||
};
|
||||
|
||||
return {
|
||||
...previewPayload,
|
||||
hasChanges: itemsSection.changes.some(change => change.changed) || itemUnitsSection.changes.some(change => change.changed),
|
||||
hasChanges:
|
||||
itemsSection.changes.some(change => change.changed) ||
|
||||
itemUnitsSection.changes.some(change => change.changed) ||
|
||||
itemUnits40HCSection.changes.some(change => change.changed),
|
||||
previewToken: buildPreviewHash({
|
||||
articleNo: mapping.articleNo,
|
||||
items: itemsSection.changes,
|
||||
itemUnitsOfMeasure: itemUnitsSection.changes,
|
||||
itemUnits40HC: itemUnits40HCSection.changes,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -496,6 +537,38 @@ async function applyItemUnitsSection(config, token, snapshot, preview, context)
|
||||
return { applied: true, url };
|
||||
}
|
||||
|
||||
async function applyItemUnits40HCSection(config, token, snapshot, preview, context) {
|
||||
const changedFields = preview.itemUnits40HC.changes.filter(change => change.changed);
|
||||
if (changedFields.length === 0) {
|
||||
return { applied: false, reason: 'No itemUnits40HC changes' };
|
||||
}
|
||||
|
||||
if (!config.itemUnits40HCWriteUrlTemplate && !config.itemUnitsWriteUrlTemplate) {
|
||||
return { applied: false, reason: 'itemUnits40HC write template not configured' };
|
||||
}
|
||||
|
||||
const urlTemplate = config.itemUnits40HCWriteUrlTemplate || config.itemUnitsWriteUrlTemplate;
|
||||
const writeMethod = preview.itemUnits40HC.writeMethod || 'PATCH';
|
||||
const hcContext = {
|
||||
...context,
|
||||
itemUnitsCode: '40HC',
|
||||
itemUnits40HCCode: '40HC',
|
||||
itemUnitsPayload: preview.itemUnits40HC.desired,
|
||||
};
|
||||
const url = renderTemplate(urlTemplate, hcContext);
|
||||
const payload = renderJsonBody(preview.itemUnits40HC.writeBodyTemplate, hcContext, buildChangedPayload(preview.itemUnits40HC.changes));
|
||||
|
||||
await renderAndPatchRecord({
|
||||
token,
|
||||
url,
|
||||
method: writeMethod,
|
||||
body: payload,
|
||||
etag: snapshot.itemUnits40HCEtag || snapshot.itemUnits40HC?.['@odata.etag'] || '*',
|
||||
});
|
||||
|
||||
return { applied: true, url };
|
||||
}
|
||||
|
||||
export async function applyBusinessCentralSync(config, token, headers, row, previewToken) {
|
||||
const mapping = buildBusinessCentralMappingPreview(headers, row);
|
||||
const snapshot = await fetchBusinessCentralSnapshot(config, token, mapping.articleNo);
|
||||
@@ -508,6 +581,13 @@ export async function applyBusinessCentralSync(config, token, headers, row, prev
|
||||
throw error;
|
||||
}
|
||||
|
||||
const hasItemUnits40HCChanges = preview.itemUnits40HC.changes.some(change => change.changed);
|
||||
if (hasItemUnits40HCChanges && !snapshot.itemUnits40HC) {
|
||||
const error = new Error(`BC itemUnits40HC row missing for ${mapping.articleNo}. This BC API cannot update these fields until the row exists or BC exposes an upsert action.`);
|
||||
error.statusCode = 409;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (previewToken && previewToken !== preview.previewToken) {
|
||||
const error = new Error('Preview token mismatch. BC data changed or preview is stale.');
|
||||
error.statusCode = 409;
|
||||
@@ -520,20 +600,24 @@ export async function applyBusinessCentralSync(config, token, headers, row, prev
|
||||
articleNo: mapping.articleNo,
|
||||
itemNo: mapping.articleNo,
|
||||
itemUnitsCode: config.itemUnitsCode || 'OUTER',
|
||||
itemUnits40HCCode: '40HC',
|
||||
systemId: snapshot.item?.systemId || '',
|
||||
cpnpNo: mapping.itemsPayload.cpnpNo,
|
||||
itemsPayload: preview.items.desired,
|
||||
itemUnitsPayload: preview.itemUnitsOfMeasure.desired,
|
||||
itemUnits40HCPayload: preview.itemUnits40HC.desired,
|
||||
...preview.items.desired,
|
||||
...preview.itemUnitsOfMeasure.desired,
|
||||
...preview.itemUnits40HC.desired,
|
||||
};
|
||||
|
||||
const results = {
|
||||
items: await applyItemsSection(config, token, snapshot, preview, context),
|
||||
itemUnitsOfMeasure: await applyItemUnitsSection(config, token, snapshot, preview, context),
|
||||
itemUnits40HC: await applyItemUnits40HCSection(config, token, snapshot, preview, context),
|
||||
};
|
||||
|
||||
if (results.items.applied || results.itemUnitsOfMeasure.applied) {
|
||||
if (results.items.applied || results.itemUnitsOfMeasure.applied || results.itemUnits40HC.applied) {
|
||||
const verificationSnapshot = await fetchBusinessCentralSnapshot(config, token, mapping.articleNo);
|
||||
|
||||
if (results.items.applied) {
|
||||
@@ -565,6 +649,21 @@ export async function applyBusinessCentralSync(config, token, headers, row, prev
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (results.itemUnits40HC.applied) {
|
||||
const hcMismatches = preview.itemUnits40HC.changes
|
||||
.filter(change => change.changed)
|
||||
.map(change => ({
|
||||
...change,
|
||||
before: verificationSnapshot.itemUnits40HC ? verificationSnapshot.itemUnits40HC[change.targetField] : undefined,
|
||||
}))
|
||||
.filter(change => normalizeForComparison(change.targetField, change.before) !== normalizeForComparison(change.targetField, change.after));
|
||||
if (hcMismatches.length > 0) {
|
||||
const error = new Error(`BC verification failed for itemUnits40HC: ${describeUnappliedChanges('itemUnits40HC', hcMismatches)}`);
|
||||
error.statusCode = 409;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -573,8 +672,10 @@ export async function applyBusinessCentralSync(config, token, headers, row, prev
|
||||
previewToken: preview.previewToken,
|
||||
results,
|
||||
preview,
|
||||
warning: preview.itemUnitsOfMeasure.changes.some(change => change.changed) && !preview.itemUnitsOfMeasure.supported
|
||||
warning: (preview.itemUnitsOfMeasure.changes.some(change => change.changed) && !preview.itemUnitsOfMeasure.supported)
|
||||
? (preview.itemUnitsOfMeasure.supportReason || 'itemUnitsOfMeasure sync is not supported by this BC API yet; preview only')
|
||||
: (preview.itemUnits40HC.changes.some(change => change.changed) && !preview.itemUnits40HC.supported)
|
||||
? (preview.itemUnits40HC.supportReason || 'itemUnits40HC sync is not supported by this BC API yet; preview only')
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -170,13 +170,6 @@ export function MatrixView({ data, headers, rowStatuses }: MatrixViewProps) {
|
||||
setBcValidationError(null);
|
||||
setBcValidationWarning(null);
|
||||
try {
|
||||
const hasWritableItems = bcValidationResult.items.changes.some((change: any) => change.changed);
|
||||
const hasOnlyUomChanges = !hasWritableItems && bcValidationResult.itemUnitsOfMeasure.changes.some((change: any) => change.changed);
|
||||
if (hasOnlyUomChanges) {
|
||||
setBcValidationWarning(bcValidationResult.itemUnitsOfMeasure.supportReason || 'itemUnitsOfMeasure sync is preview only');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await applyBusinessCentralSync(headers, bcPreviewRow, bcValidationResult.previewToken);
|
||||
if (!result.success) {
|
||||
if (isPreviewTokenMismatchError(result.error)) {
|
||||
@@ -417,8 +410,8 @@ export function MatrixView({ data, headers, rowStatuses }: MatrixViewProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
{(['items', 'itemUnitsOfMeasure'] as const).map(sectionKey => {
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-4">
|
||||
{(['items', 'itemUnitsOfMeasure', 'itemUnits40HC'] as const).map(sectionKey => {
|
||||
const section = bcValidationResult[sectionKey];
|
||||
const changed = section.changes.filter((change: any) => change.changed);
|
||||
|
||||
@@ -472,7 +465,7 @@ export function MatrixView({ data, headers, rowStatuses }: MatrixViewProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-4">
|
||||
<div className="rounded-lg border border-slate-700 bg-slate-950/60 p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
@@ -514,6 +507,27 @@ export function MatrixView({ data, headers, rowStatuses }: MatrixViewProps) {
|
||||
{JSON.stringify(bcMappingPreview.itemUnitsOfMeasurePayload, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-slate-700 bg-slate-950/60 p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white">API `itemUnitsOfMeasure2` 40HC preview</h3>
|
||||
<p className="text-xs text-slate-400">SKU {bcMappingPreview.articleNo}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{bcMappingPreview.itemUnits40HCFields.map(field => (
|
||||
<div key={field.targetField} className="grid grid-cols-3 gap-3 text-xs">
|
||||
<div className="text-slate-400">{field.sourceLabel}</div>
|
||||
<div className="text-slate-500 truncate">{field.targetField}</div>
|
||||
<div className="text-white truncate">{String(field.value ?? '—')}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<pre className="mt-4 text-[11px] leading-relaxed bg-slate-950 border border-slate-800 rounded p-3 overflow-auto text-slate-200">
|
||||
{JSON.stringify(bcMappingPreview.itemUnits40HCPayload, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -27,7 +27,7 @@ export interface BCFieldChange {
|
||||
}
|
||||
|
||||
export interface BCSyncPreviewSection {
|
||||
type: 'items' | 'itemUnitsOfMeasure';
|
||||
type: 'items' | 'itemUnitsOfMeasure' | 'itemUnits40HC';
|
||||
desired: Record<string, any>;
|
||||
current: Record<string, any> | null;
|
||||
changes: BCFieldChange[];
|
||||
@@ -46,6 +46,7 @@ export interface BCSyncPreviewResult {
|
||||
articleNo: string;
|
||||
items: BCSyncPreviewSection;
|
||||
itemUnitsOfMeasure: BCSyncPreviewSection;
|
||||
itemUnits40HC: BCSyncPreviewSection;
|
||||
hasChanges: boolean;
|
||||
previewToken: string;
|
||||
error?: string;
|
||||
@@ -59,6 +60,7 @@ export interface BCSyncApplyResult {
|
||||
results?: {
|
||||
items: { applied: boolean; reason?: string; url?: string };
|
||||
itemUnitsOfMeasure: { applied: boolean; reason?: string; url?: string };
|
||||
itemUnits40HC: { applied: boolean; reason?: string; url?: string };
|
||||
};
|
||||
error?: string;
|
||||
warning?: string;
|
||||
|
||||
@@ -11,8 +11,10 @@ export interface BusinessCentralMappingPreview {
|
||||
articleNo: string;
|
||||
itemsPayload: Record<string, any>;
|
||||
itemUnitsOfMeasurePayload: Record<string, any>;
|
||||
itemUnits40HCPayload: Record<string, any>;
|
||||
itemsFields: MappingFieldPreview[];
|
||||
itemUnitsFields: MappingFieldPreview[];
|
||||
itemUnits40HCFields: MappingFieldPreview[];
|
||||
}
|
||||
|
||||
function findHeaderIndex(headers: string[], patterns: string[]): number {
|
||||
@@ -81,6 +83,11 @@ export function buildBusinessCentralMappingPreview(headers: string[], row: Excel
|
||||
const cpnpIdx = findHeaderIndex(headers, ['cpnp']);
|
||||
|
||||
const unitsOuterIdx = findHeaderIndex(headers, ['units', 'outer']);
|
||||
const units40HqIdx = (() => {
|
||||
const hqIdx = findHeaderIndex(headers, ['40', 'hq']);
|
||||
if (hqIdx >= 0) return hqIdx;
|
||||
return findHeaderIndex(headers, ['40', 'hc']);
|
||||
})();
|
||||
const outerWIdx = findHeaderIndex(headers, ['outer', 'w']);
|
||||
const outerLIdx = findHeaderIndex(headers, ['outer', 'l']);
|
||||
const outerHIdx = findHeaderIndex(headers, ['outer', 'h']);
|
||||
@@ -108,10 +115,17 @@ export function buildBusinessCentralMappingPreview(headers: string[], row: Excel
|
||||
height: toBcDecimal(getValue(row, outerHIdx)),
|
||||
};
|
||||
|
||||
const itemUnits40HCPayload = {
|
||||
itemNo: getValue(row, articleNoIdx),
|
||||
code: '40HC',
|
||||
qtyPerUnitOfMeasure: toBcDecimal(getValue(row, units40HqIdx)),
|
||||
};
|
||||
|
||||
return {
|
||||
articleNo,
|
||||
itemsPayload,
|
||||
itemUnitsOfMeasurePayload,
|
||||
itemUnits40HCPayload,
|
||||
itemsFields: [
|
||||
makeFieldPreview('Article No.', articleNoIdx, 'no', itemsPayload.no),
|
||||
makeFieldPreview('Article Details - English', articleDetailsEnIdx, 'articleDetailsEnglish', itemsPayload.articleDetailsEnglish),
|
||||
@@ -130,5 +144,9 @@ export function buildBusinessCentralMappingPreview(headers: string[], row: Excel
|
||||
makeFieldPreview('Outer L (cm)', outerLIdx, 'length', itemUnitsOfMeasurePayload.length),
|
||||
makeFieldPreview('Outer H (cm)', outerHIdx, 'height', itemUnitsOfMeasurePayload.height),
|
||||
],
|
||||
itemUnits40HCFields: [
|
||||
makeFieldPreview('Article No.', articleNoIdx, 'itemNo', itemUnits40HCPayload.itemNo),
|
||||
makeFieldPreview('Units 40FT HQ', units40HqIdx, 'qtyPerUnitOfMeasure', itemUnits40HCPayload.qtyPerUnitOfMeasure),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user