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, ...(Array.isArray(mappedVarient.aspects) ? { aspects: mappedVarient.aspects } : {}), }; 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, marketplace); 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 };