Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
This commit introduces a new module, `auditOwner.js`, which includes functions for resolving audit owner details and generating display names for actors. The `resolveAuditOwner` function determines the owner type based on the actor's object type, defaulting to 'user' if not specified. The `actorDisplayName` function formats the display name based on the actor's properties, enhancing the clarity of audit logs. Additionally, the `AUDIT_OWNER_TYPES` constant is exported for use in other modules. Updates to existing files incorporate these new functions for improved audit logging and notification handling.
483 lines
15 KiB
JavaScript
483 lines
15 KiB
JavaScript
import { clientModel } from '../database/schemas/sales/client.schema.js';
|
|
import { salesOrderModel } from '../database/schemas/sales/salesorder.schema.js';
|
|
import { listingModel } from '../database/schemas/sales/listing.schema.js';
|
|
import { listingVarientModel } from '../database/schemas/sales/listingvarient.schema.js';
|
|
import { marketplaceModel } from '../database/schemas/sales/marketplace.schema.js';
|
|
import { orderItemModel } from '../database/schemas/inventory/orderitem.schema.js';
|
|
import { shipmentModel } from '../database/schemas/inventory/shipment.schema.js';
|
|
import { marketplaceEventModel } from '../database/schemas/sales/marketplaceevent.schema.js';
|
|
import { productSkuModel } from '../database/schemas/management/productsku.schema.js';
|
|
import { productModel } from '../database/schemas/management/product.schema.js';
|
|
import { editObject, newObject } from '../database/database.js';
|
|
import { marketplaceActor, marketplaceSku } from './marketplaces/ids.js';
|
|
import config from '../config.js';
|
|
import log4js from 'log4js';
|
|
|
|
const logger = log4js.getLogger('Marketplace Sync');
|
|
logger.level = config.server.logLevel;
|
|
|
|
function externalIdOf(mapped = {}) {
|
|
return mapped.externalReference || mapped.externalId;
|
|
}
|
|
|
|
function stateType(mapped) {
|
|
return mapped?.state?.type || mapped?.state;
|
|
}
|
|
|
|
async function upsertClient(marketplace, clientData, actor) {
|
|
if (!clientData) return null;
|
|
const marketplaceId = marketplace._id;
|
|
let client = null;
|
|
const externalReference = externalIdOf(clientData);
|
|
if (externalReference) {
|
|
client = await clientModel.findOne({ marketplace: marketplaceId, externalReference });
|
|
}
|
|
if (!client && clientData.email) {
|
|
client = await clientModel.findOne({ marketplace: marketplaceId, email: clientData.email });
|
|
}
|
|
if (!client && clientData.name) {
|
|
client = await clientModel.findOne({ marketplace: marketplaceId, name: clientData.name });
|
|
}
|
|
|
|
const payload = {
|
|
...clientData,
|
|
marketplace: marketplaceId,
|
|
active: true,
|
|
...(externalReference ? { externalReference } : {}),
|
|
};
|
|
delete payload.externalId;
|
|
|
|
if (client) {
|
|
return editObject({
|
|
model: clientModel,
|
|
id: client._id,
|
|
updateData: payload,
|
|
user: actor,
|
|
});
|
|
}
|
|
|
|
return newObject({
|
|
model: clientModel,
|
|
newData: payload,
|
|
user: actor,
|
|
});
|
|
}
|
|
|
|
async function upsertOrderItems(salesOrder, externalOrder, provider, actor) {
|
|
if (typeof provider.mapOrderLineItems !== 'function') return;
|
|
const lines = provider.mapOrderLineItems(externalOrder) || [];
|
|
for (const line of lines) {
|
|
const lineRef = externalIdOf(line);
|
|
if (!lineRef) continue;
|
|
|
|
let listingVarient = null;
|
|
if (line.sku) {
|
|
listingVarient = await listingVarientModel.findOne({
|
|
$or: [{ externalReference: line.sku }, { _reference: line.sku }],
|
|
});
|
|
}
|
|
|
|
const payload = {
|
|
orderType: 'salesOrder',
|
|
order: salesOrder._id,
|
|
name: line.name || line.sku || 'Marketplace item',
|
|
itemType: line.itemType || 'product',
|
|
quantity: line.quantity || 1,
|
|
itemAmount: line.itemAmount ?? 0,
|
|
totalAmount: line.totalAmount ?? 0,
|
|
totalAmountWithTax: line.totalAmountWithTax ?? line.totalAmount ?? 0,
|
|
externalReference: lineRef,
|
|
listing: listingVarient?.listing,
|
|
listingVarient: listingVarient?._id,
|
|
item: listingVarient?.product,
|
|
sku: listingVarient?.productSku,
|
|
state: { type: stateType(salesOrder) === 'cancelled' ? 'cancelled' : 'ordered' },
|
|
};
|
|
|
|
const existing = await orderItemModel.findOne({
|
|
order: salesOrder._id,
|
|
externalReference: lineRef,
|
|
});
|
|
if (existing) {
|
|
await editObject({
|
|
model: orderItemModel,
|
|
id: existing._id,
|
|
updateData: payload,
|
|
user: actor,
|
|
});
|
|
} else {
|
|
await newObject({
|
|
model: orderItemModel,
|
|
newData: payload,
|
|
user: actor,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
async function upsertOrderShipments(salesOrder, externalOrder, provider, actor) {
|
|
if (typeof provider.mapOrderShipments !== 'function') return;
|
|
const shipments = provider.mapOrderShipments(externalOrder) || [];
|
|
for (const mapped of shipments) {
|
|
const shipmentRef = externalIdOf(mapped);
|
|
if (!shipmentRef) continue;
|
|
const payload = {
|
|
orderType: 'salesOrder',
|
|
order: salesOrder._id,
|
|
trackingNumber: mapped.trackingNumber || '',
|
|
amount: mapped.amount ?? 0,
|
|
amountWithTax: mapped.amountWithTax ?? mapped.amount ?? 0,
|
|
externalReference: shipmentRef,
|
|
state: mapped.state || { type: 'planned' },
|
|
shippedAt: mapped.shippedAt,
|
|
};
|
|
|
|
const existing = await shipmentModel.findOne({
|
|
order: salesOrder._id,
|
|
externalReference: shipmentRef,
|
|
});
|
|
if (existing) {
|
|
await editObject({
|
|
model: shipmentModel,
|
|
id: existing._id,
|
|
updateData: payload,
|
|
user: actor,
|
|
});
|
|
} else {
|
|
await newObject({
|
|
model: shipmentModel,
|
|
newData: payload,
|
|
user: actor,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function upsertExternalOrder(marketplace, provider, externalOrder, actor) {
|
|
const mapped = provider.mapOrderToSalesOrder(externalOrder);
|
|
const clientData = provider.mapBuyerToClient(externalOrder);
|
|
const externalReference = externalIdOf(mapped);
|
|
if (!externalReference) {
|
|
throw new Error('Mapped sales order is missing externalReference');
|
|
}
|
|
|
|
const client = await upsertClient(marketplace, clientData, actor);
|
|
const existingOrder = await salesOrderModel.findOne({
|
|
marketplace: marketplace._id,
|
|
externalReference,
|
|
});
|
|
|
|
const lifecycle = {};
|
|
const type = mapped.state?.type;
|
|
if (type === 'confirmed' || type === 'shipped' || type === 'delivered') {
|
|
lifecycle.confirmedAt = existingOrder?.confirmedAt || new Date();
|
|
}
|
|
if (type === 'cancelled') {
|
|
lifecycle.cancelledAt = existingOrder?.cancelledAt || new Date();
|
|
}
|
|
|
|
const orderPayload = {
|
|
client: client?._id,
|
|
marketplace: marketplace._id,
|
|
externalReference,
|
|
state: mapped.state,
|
|
totalAmount: mapped.totalAmount,
|
|
totalAmountWithTax: mapped.totalAmountWithTax,
|
|
shippingAmount: mapped.shippingAmount,
|
|
shippingAmountWithTax: mapped.shippingAmountWithTax,
|
|
grandTotalAmount: mapped.grandTotalAmount,
|
|
totalTaxAmount: mapped.totalTaxAmount,
|
|
...lifecycle,
|
|
};
|
|
|
|
let salesOrder = existingOrder;
|
|
if (existingOrder) {
|
|
salesOrder = await editObject({
|
|
model: salesOrderModel,
|
|
id: existingOrder._id,
|
|
updateData: orderPayload,
|
|
user: actor,
|
|
});
|
|
} else {
|
|
salesOrder = await newObject({
|
|
model: salesOrderModel,
|
|
newData: orderPayload,
|
|
user: actor,
|
|
});
|
|
}
|
|
|
|
await upsertOrderItems(salesOrder, externalOrder, provider, actor);
|
|
await upsertOrderShipments(salesOrder, externalOrder, provider, actor);
|
|
return { salesOrder, action: existingOrder ? 'updated' : 'created', externalReference };
|
|
}
|
|
|
|
async function matchProductSku(mapped) {
|
|
const skuCodes = [
|
|
mapped.externalReference,
|
|
...(mapped.varients || []).map((v) => v.externalReference || v._reference),
|
|
].filter(Boolean);
|
|
|
|
for (const code of skuCodes) {
|
|
const sku = await productSkuModel.findOne({
|
|
$or: [{ _reference: code }, { barcode: code }],
|
|
});
|
|
if (sku) return sku;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function upsertInboundListing(marketplace, mapped, actor) {
|
|
const externalReference = externalIdOf(mapped);
|
|
if (!externalReference) {
|
|
logger.warn('Skipping inbound listing without externalReference');
|
|
return { action: 'skipped' };
|
|
}
|
|
|
|
const defaults = marketplace.config?.defaults || {};
|
|
const productSku = await matchProductSku(mapped);
|
|
const product = productSku
|
|
? await productModel.findById(productSku.product).lean()
|
|
: null;
|
|
|
|
const existing = await listingModel.findOne({
|
|
marketplace: marketplace._id,
|
|
externalReference,
|
|
});
|
|
|
|
const payload = {
|
|
title: mapped.title,
|
|
url: mapped.url,
|
|
price: mapped.price,
|
|
currency: mapped.currency,
|
|
description: mapped.description,
|
|
externalReference,
|
|
lastSyncedAt: new Date(),
|
|
state: mapped.state || { type: 'active' },
|
|
marketplace: marketplace._id,
|
|
};
|
|
|
|
if (product) {
|
|
payload.product = product._id;
|
|
payload.vendor = product.vendor;
|
|
}
|
|
|
|
let listing = existing;
|
|
if (existing) {
|
|
listing = await editObject({
|
|
model: listingModel,
|
|
id: existing._id,
|
|
updateData: payload,
|
|
user: actor,
|
|
});
|
|
} else {
|
|
const vendor = payload.vendor || defaults.vendor;
|
|
const stockLocation = defaults.stockLocation;
|
|
const courierServices = defaults.courierServices || [];
|
|
if (!vendor || !stockLocation) {
|
|
logger.warn(
|
|
`Skipping inbound listing ${externalReference}: marketplace defaults.vendor and defaults.stockLocation are required to create listings`
|
|
);
|
|
return { action: 'skipped', externalReference };
|
|
}
|
|
listing = await newObject({
|
|
model: listingModel,
|
|
newData: {
|
|
...payload,
|
|
vendor,
|
|
stockLocation,
|
|
courierServices,
|
|
},
|
|
user: actor,
|
|
});
|
|
}
|
|
|
|
for (const mappedVarient of mapped.varients || []) {
|
|
const varientRef = mappedVarient.externalReference || mappedVarient._reference;
|
|
if (!varientRef) continue;
|
|
const varientPayload = {
|
|
listing: listing._id,
|
|
externalReference: varientRef,
|
|
price: mappedVarient.price,
|
|
currency: mappedVarient.currency,
|
|
state: mappedVarient.state || mapped.state || { type: 'active' },
|
|
lastSyncedAt: new Date(),
|
|
product: product?._id || listing.product,
|
|
productSku: productSku?._id,
|
|
};
|
|
const existingVarient = await listingVarientModel.findOne({
|
|
listing: listing._id,
|
|
externalReference: varientRef,
|
|
});
|
|
if (existingVarient) {
|
|
await editObject({
|
|
model: listingVarientModel,
|
|
id: existingVarient._id,
|
|
updateData: varientPayload,
|
|
user: actor,
|
|
});
|
|
} else {
|
|
await newObject({
|
|
model: listingVarientModel,
|
|
newData: varientPayload,
|
|
user: actor,
|
|
});
|
|
}
|
|
}
|
|
|
|
return { action: existing ? 'updated' : 'created', listing };
|
|
}
|
|
|
|
export async function importExternalItems(marketplace, provider, actor) {
|
|
if (typeof provider.syncItems !== 'function' || typeof provider.mapProductToListing !== 'function') {
|
|
return [];
|
|
}
|
|
const externalItems = await provider.syncItems(marketplace);
|
|
const results = [];
|
|
for (const item of externalItems || []) {
|
|
try {
|
|
const mapped = provider.mapProductToListing(item);
|
|
const result = await upsertInboundListing(marketplace, mapped, actor);
|
|
results.push(result);
|
|
} catch (err) {
|
|
logger.warn(`Failed to import marketplace listing: ${err.message}`);
|
|
results.push({ action: 'error', error: err.message });
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
export async function recordMarketplaceEvent(marketplace, classified, actor) {
|
|
const externalReference =
|
|
classified.notificationId ||
|
|
`${classified.action}:${classified.orderId || classified.itemId || classified.topic || Date.now()}`;
|
|
const existing = await marketplaceEventModel.findOne({
|
|
marketplace: marketplace._id,
|
|
externalReference,
|
|
});
|
|
if (existing) {
|
|
return existing;
|
|
}
|
|
return newObject({
|
|
model: marketplaceEventModel,
|
|
newData: {
|
|
marketplace: marketplace._id,
|
|
externalReference,
|
|
topic: classified.topic || classified.action || 'unknown',
|
|
status: 'received',
|
|
},
|
|
user: actor,
|
|
});
|
|
}
|
|
|
|
export async function markMarketplaceEvent(event, status, actor, error) {
|
|
if (!event?._id) return;
|
|
await editObject({
|
|
model: marketplaceEventModel,
|
|
id: event._id,
|
|
updateData: {
|
|
status,
|
|
processedAt: status === 'processed' ? new Date() : event.processedAt,
|
|
error: error || undefined,
|
|
},
|
|
user: actor,
|
|
});
|
|
}
|
|
|
|
export async function applyWebhookAction(marketplace, provider, classified, actor) {
|
|
const event = await recordMarketplaceEvent(marketplace, classified, actor);
|
|
if (event.status === 'processed') {
|
|
return { ...classified, duplicate: true };
|
|
}
|
|
|
|
try {
|
|
switch (classified.action) {
|
|
case 'orderCreate':
|
|
case 'orderUpdate':
|
|
case 'orderCancel': {
|
|
if (classified.orderId && typeof provider.getOrder === 'function') {
|
|
const order = await provider.getOrder(marketplace, classified.orderId);
|
|
await upsertExternalOrder(marketplace, provider, order, actor);
|
|
}
|
|
break;
|
|
}
|
|
case 'productUpdate':
|
|
case 'productEnded': {
|
|
await importExternalItems(marketplace, provider, actor);
|
|
break;
|
|
}
|
|
case 'accountDeletion': {
|
|
if (classified.userId) {
|
|
const client = await clientModel.findOne({
|
|
marketplace: marketplace._id,
|
|
$or: [{ externalReference: classified.userId }, { name: classified.userId }],
|
|
});
|
|
if (client) {
|
|
await editObject({
|
|
model: clientModel,
|
|
id: client._id,
|
|
updateData: {
|
|
name: 'Deleted marketplace user',
|
|
email: '',
|
|
phone: '',
|
|
address: {},
|
|
},
|
|
user: actor,
|
|
});
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'disconnect': {
|
|
await editObject({
|
|
model: marketplaceModel,
|
|
id: marketplace._id,
|
|
updateData: { connected: false, state: { type: 'disconnected' } },
|
|
user: actor,
|
|
});
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
await markMarketplaceEvent(event, 'processed', actor);
|
|
} catch (err) {
|
|
await markMarketplaceEvent(event, 'failed', actor, err.message);
|
|
throw err;
|
|
}
|
|
|
|
return classified;
|
|
}
|
|
|
|
export async function pushShipmentFulfillment(marketplace, provider, shipment, actor) {
|
|
if (typeof provider.createShippingFulfillment !== 'function') {
|
|
return;
|
|
}
|
|
const orderId = shipment.order?._id || shipment.order;
|
|
const salesOrder = await salesOrderModel.findById(orderId).lean();
|
|
if (!salesOrder?.externalReference) {
|
|
return;
|
|
}
|
|
const orderItems = await orderItemModel
|
|
.find({ order: salesOrder._id, shipment: shipment._id, orderType: 'salesOrder' })
|
|
.lean();
|
|
await provider.createShippingFulfillment(marketplace, {
|
|
orderId: salesOrder.externalReference,
|
|
trackingNumber: shipment.trackingNumber,
|
|
shippingCarrierCode: shipment.courierService?._reference || shipment.courierService?.code,
|
|
lineItems: orderItems.map((item) => ({
|
|
lineItemId: item.externalReference,
|
|
quantity: item.quantity,
|
|
})),
|
|
});
|
|
if (shipment._id && !shipment.externalReference) {
|
|
await editObject({
|
|
model: shipmentModel,
|
|
id: shipment._id,
|
|
updateData: { lastSyncedAt: new Date() },
|
|
user: actor,
|
|
});
|
|
}
|
|
}
|
|
|
|
export { marketplaceActor, marketplaceSku };
|