mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 13:35:25 +02:00
191 lines
6.3 KiB
JavaScript
191 lines
6.3 KiB
JavaScript
import * as XLSX from 'xlsx';
|
|
|
|
export function getBcConfig(env = process.env) {
|
|
const legacyWriteMethod = String(env.BC_WRITE_METHOD || 'PATCH').toUpperCase();
|
|
const legacyWriteUrlTemplate = env.BC_WRITE_URL_TEMPLATE || `{{itemsUrl}}('{{itemNo}}')`;
|
|
const legacyWriteBodyTemplate = env.BC_WRITE_BODY_TEMPLATE || JSON.stringify({ cpnpNo: '{{cpnpNo}}' });
|
|
|
|
return {
|
|
tenantId: env.BC_TENANT_ID || 'fab724f7-6b6d-4e3b-86e3-8c1e05e36b2a',
|
|
clientId: env.BC_CLIENT_ID || '6f832138-cb48-43e7-8601-efca120b45dc',
|
|
clientSecret: env.BC_CLIENT_SECRET,
|
|
companyId: env.BC_COMPANY_ID || '2acec35c-7d06-ed11-82f8-0022485ceea3',
|
|
writeMethod: legacyWriteMethod,
|
|
writeUrlTemplate: legacyWriteUrlTemplate,
|
|
writeBodyTemplate: legacyWriteBodyTemplate,
|
|
itemsWriteMethod: String(env.BC_ITEMS_WRITE_METHOD || legacyWriteMethod).toUpperCase(),
|
|
itemsWriteUrlTemplate: env.BC_ITEMS_WRITE_URL_TEMPLATE || legacyWriteUrlTemplate,
|
|
itemsWriteBodyTemplate: env.BC_ITEMS_WRITE_BODY_TEMPLATE || null,
|
|
itemUnitsCode: env.BC_UOM_CODE || 'OUTER',
|
|
itemUnitsWriteMethod: env.BC_UOM_WRITE_METHOD ? String(env.BC_UOM_WRITE_METHOD).toUpperCase() : 'PATCH',
|
|
itemUnitsWriteUrlTemplate: env.BC_UOM_WRITE_URL_TEMPLATE || `{{itemUnitsOfMeasureUrl}}(itemNo='{{itemNo}}',code='{{itemUnitsCode}}')`,
|
|
itemUnitsWriteBodyTemplate: env.BC_UOM_WRITE_BODY_TEMPLATE || null,
|
|
};
|
|
}
|
|
|
|
export function getTokenUrl(config) {
|
|
return `https://login.microsoftonline.com/${config.tenantId}/oauth2/v2.0/token`;
|
|
}
|
|
|
|
export function getItemsUrl(config) {
|
|
return `https://api.businesscentral.dynamics.com/v2.0/${config.tenantId}/production/api/craze/integrations/v1.0/companies(${config.companyId})/items`;
|
|
}
|
|
|
|
export function getItemUnitsOfMeasureUrl(config) {
|
|
return `https://api.businesscentral.dynamics.com/v2.0/${config.tenantId}/production/api/craze/integrations/v1.0/companies(${config.companyId})/itemUnitsOfMeasure2`;
|
|
}
|
|
|
|
let tokenCache = { token: null, expiresAt: 0 };
|
|
|
|
export async function getBCToken(config) {
|
|
const now = Date.now();
|
|
if (tokenCache.token && now < tokenCache.expiresAt) {
|
|
return tokenCache.token;
|
|
}
|
|
|
|
if (!config.clientSecret) {
|
|
throw new Error('BC_CLIENT_SECRET env var not set');
|
|
}
|
|
|
|
const body = new URLSearchParams({
|
|
grant_type: 'client_credentials',
|
|
client_id: config.clientId,
|
|
client_secret: config.clientSecret,
|
|
scope: 'https://api.businesscentral.dynamics.com/.default',
|
|
});
|
|
|
|
const res = await fetch(getTokenUrl(config), {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body,
|
|
});
|
|
|
|
const data = await res.json();
|
|
if (!data.access_token) {
|
|
throw new Error('BC token error: ' + JSON.stringify(data));
|
|
}
|
|
|
|
tokenCache = { token: data.access_token, expiresAt: now + 55 * 60 * 1000 };
|
|
return data.access_token;
|
|
}
|
|
|
|
export async function fetchAllItems(config, token) {
|
|
const items = [];
|
|
let url = `${getItemsUrl(config)}?$top=1000`;
|
|
|
|
while (url) {
|
|
const res = await fetch(url, {
|
|
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const txt = await res.text();
|
|
throw new Error(`BC GET items failed (${res.status}): ${txt}`);
|
|
}
|
|
|
|
const json = await res.json();
|
|
if (Array.isArray(json.value)) {
|
|
items.push(...json.value);
|
|
}
|
|
|
|
url = json['@odata.nextLink'] || null;
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
export async function findItem(config, token, articleNo) {
|
|
const filter = encodeURIComponent(`no eq '${articleNo}'`);
|
|
const url = `${getItemsUrl(config)}?$filter=${filter}&$select=systemId,no,cpnpNo`;
|
|
const res = await fetch(url, {
|
|
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
|
|
});
|
|
if (!res.ok) {
|
|
const txt = await res.text();
|
|
throw new Error(`BC GET items failed (${res.status}): ${txt}`);
|
|
}
|
|
const json = await res.json();
|
|
const items = json.value || [];
|
|
console.log('[bc-proxy] GET items result:', JSON.stringify(items));
|
|
if (items.length === 0) throw new Error(`Item not found in BC: ${articleNo}`);
|
|
return items[0];
|
|
}
|
|
|
|
export async function patchItemCpnpNo(config, token, item, cpnpNo) {
|
|
const etag = item['@odata.etag'] || '*';
|
|
const itemsUrl = getItemsUrl(config);
|
|
const url = config.writeUrlTemplate
|
|
.replaceAll('{{itemsUrl}}', itemsUrl)
|
|
.replaceAll('{{tenantId}}', config.tenantId)
|
|
.replaceAll('{{companyId}}', config.companyId)
|
|
.replaceAll('{{itemNo}}', encodeURIComponent(String(item.no)))
|
|
.replaceAll('{{systemId}}', encodeURIComponent(String(item.systemId || '')))
|
|
.replaceAll('{{cpnpNo}}', String(cpnpNo));
|
|
const bodyText = config.writeBodyTemplate
|
|
.replaceAll('{{itemsUrl}}', itemsUrl)
|
|
.replaceAll('{{tenantId}}', config.tenantId)
|
|
.replaceAll('{{companyId}}', config.companyId)
|
|
.replaceAll('{{itemNo}}', String(item.no))
|
|
.replaceAll('{{systemId}}', String(item.systemId || ''))
|
|
.replaceAll('{{cpnpNo}}', String(cpnpNo));
|
|
console.log('[bc-proxy] WRITE url:', url);
|
|
const headers = {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
};
|
|
if ((config.writeMethod || 'PATCH') === 'PATCH') {
|
|
headers['If-Match'] = etag;
|
|
}
|
|
const res = await fetch(url, {
|
|
method: config.writeMethod || 'PATCH',
|
|
headers,
|
|
body: bodyText,
|
|
});
|
|
if (!res.ok) {
|
|
const txt = await res.text();
|
|
throw new Error(`BC write failed (${res.status}): ${txt}`);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function normalizeValue(value) {
|
|
if (value === null || value === undefined) return '';
|
|
if (value instanceof Date) return value.toISOString();
|
|
if (typeof value === 'object') return JSON.stringify(value);
|
|
return value;
|
|
}
|
|
|
|
export function buildWorkbook(items) {
|
|
const headers = [];
|
|
const seen = new Set();
|
|
|
|
items.forEach(item => {
|
|
Object.keys(item || {}).forEach(key => {
|
|
if (key.startsWith('@odata.')) return;
|
|
if (seen.has(key)) return;
|
|
seen.add(key);
|
|
headers.push(key);
|
|
});
|
|
});
|
|
|
|
const rows = items.map(item => {
|
|
const row = {};
|
|
headers.forEach(key => {
|
|
row[key] = normalizeValue(item?.[key]);
|
|
});
|
|
return row;
|
|
});
|
|
|
|
const ws = XLSX.utils.json_to_sheet(rows, { header: headers });
|
|
ws['!autofilter'] = {
|
|
ref: XLSX.utils.encode_range({
|
|
s: { c: 0, r: 0 },
|
|
e: { c: Math.max(headers.length - 1, 0), r: Math.max(rows.length, 0) },
|
|
}),
|
|
};
|
|
|
|
const wb = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(wb, ws, 'Items');
|
|
return wb;
|
|
}
|