import { randomUUID } from "crypto";
import { registerCommand } from "@open-mercato/shared/lib/commands";
import { LockMode } from "@mikro-orm/core";
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
import { loadCustomFieldValues } from "@open-mercato/shared/lib/crud/custom-fields";
import { setRecordCustomFields } from "@open-mercato/core/modules/entities/lib/helpers";
import { E } from "../../../generated/entities.ids.generated.js";
import { SalesOrder, SalesOrderLine, SalesShipment, SalesShipmentItem } from "../data/entities.js";
import {
  shipmentCreateSchema,
  shipmentUpdateSchema
} from "../data/validators.js";
import {
  coerceShipmentQuantity as toNumber,
  readShipmentItemsSnapshot,
  refreshShipmentItemsSnapshot,
  buildShipmentItemSnapshots
} from "../lib/shipments/snapshots.js";
import {
  cloneJson,
  ensureOrganizationScope,
  ensureSameScope,
  ensureTenantScope,
  extractUndoPayload,
  enforceSalesDocumentOptimisticLock,
  SALES_RESOURCE_KIND_ORDER
} from "./shared.js";
import { resolveDictionaryEntryValue } from "../lib/dictionaries.js";
import { resolveRedoSnapshot } from "@open-mercato/shared/lib/commands/redo";
import { emitCrudSideEffects } from "@open-mercato/shared/lib/commands/helpers";
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { createLogger } from "@open-mercato/shared/lib/logger";
const fallbackShipmentLogger = createLogger("sales").child({ component: "shipments" });
const shipmentCrudEvents = {
  module: "sales",
  entity: "shipment",
  persistent: true,
  buildPayload: (ctx) => ({
    id: ctx.identifiers.id,
    organizationId: ctx.identifiers.organizationId,
    tenantId: ctx.identifiers.tenantId
  })
};
const ADDRESS_SNAPSHOT_KEY = "shipmentAddressSnapshot";
const buildShipmentCreateRedoInput = (snapshot) => ({
  orderId: snapshot.orderId,
  organizationId: snapshot.organizationId,
  tenantId: snapshot.tenantId,
  shipmentNumber: snapshot.shipmentNumber ?? void 0,
  shippingMethodId: snapshot.shippingMethodId ?? void 0,
  statusEntryId: snapshot.statusEntryId ?? void 0,
  carrierName: snapshot.carrierName ?? void 0,
  trackingNumbers: snapshot.trackingNumbers ?? void 0,
  shippedAt: snapshot.shippedAt ? new Date(snapshot.shippedAt) : void 0,
  deliveredAt: snapshot.deliveredAt ? new Date(snapshot.deliveredAt) : void 0,
  weightValue: snapshot.weightValue ?? void 0,
  weightUnit: snapshot.weightUnit ?? void 0,
  declaredValueNet: snapshot.declaredValueNet ?? void 0,
  declaredValueGross: snapshot.declaredValueGross ?? void 0,
  currencyCode: snapshot.currencyCode ?? void 0,
  notes: snapshot.notesText ?? void 0,
  metadata: snapshot.metadata ? cloneJson(snapshot.metadata) : void 0,
  customFields: snapshot.customFields ? cloneJson(snapshot.customFields) : void 0,
  items: snapshot.items.map((item) => ({
    orderLineId: item.orderLineId,
    quantity: item.quantity,
    metadata: item.metadata ? cloneJson(item.metadata) : void 0
  }))
});
const parseTrackingNumbers = (input) => {
  if (typeof input === "string") {
    const entries = input.split(/[\n,]/g).map((value) => value.trim()).filter((value) => value.length > 0);
    return entries.length ? entries : null;
  }
  if (Array.isArray(input)) {
    const entries = input.map((value) => typeof value === "string" ? value.trim() : "").filter((value) => value.length > 0);
    return entries.length ? entries : null;
  }
  return null;
};
const normalizeCustomFieldsInput = (input) => input && typeof input === "object" && !Array.isArray(input) ? input : {};
const resolveLogger = (ctx) => {
  const container = ctx?.container;
  if (!container) return null;
  const cradleLogger = container?.cradle?.logger ?? null;
  if (cradleLogger?.warn) return cradleLogger;
  const hasRegistration = typeof container.hasRegistration === "function" ? container.hasRegistration.bind(container) : null;
  if (hasRegistration?.("logger")) {
    const logger = container.resolve("logger");
    if (logger?.warn) return logger;
  }
  if (hasRegistration?.("coreLogger")) {
    const logger = container.resolve("coreLogger");
    if (logger?.warn) return logger;
  }
  return null;
};
const logShipmentDeleteScopeRejection = (ctx, reason, meta) => {
  const logger = resolveLogger(ctx);
  const payload = { ...meta, command: "sales.shipments.delete" };
  if (logger?.warn) {
    logger.warn(payload, reason);
    return;
  }
  fallbackShipmentLogger.warn(reason, payload);
};
async function loadShipmentSnapshot(em, id) {
  const shipment = await em.findOne(
    SalesShipment,
    { id },
    { populate: ["order", "items", "items.orderLine"] }
  );
  if (!shipment || !shipment.order) return null;
  const storedSnapshot = readShipmentItemsSnapshot(
    shipment.itemsSnapshot ?? shipment.items_snapshot ?? null
  );
  const lineMap = new Map(
    Array.from(shipment.items ?? []).map((item) => {
      const line = typeof item.orderLine === "string" ? null : item.orderLine;
      return line ? [line.id, line] : null;
    }).filter((entry) => Boolean(entry))
  );
  const fallbackItems = buildShipmentItemSnapshots(Array.from(shipment.items ?? []), { lineMap });
  const itemsSnapshot = storedSnapshot.length ? storedSnapshot : fallbackItems;
  const customFieldValues = await loadCustomFieldValues({
    em,
    entityId: E.sales.sales_shipment,
    recordIds: [shipment.id],
    tenantIdByRecord: { [shipment.id]: shipment.tenantId ?? null },
    organizationIdByRecord: { [shipment.id]: shipment.organizationId ?? null }
  });
  const customFields = customFieldValues[shipment.id];
  return {
    id: shipment.id,
    orderId: typeof shipment.order === "string" ? shipment.order : shipment.order.id,
    organizationId: shipment.organizationId,
    tenantId: shipment.tenantId,
    shipmentNumber: shipment.shipmentNumber ?? null,
    shippingMethodId: shipment.shippingMethodId ?? null,
    statusEntryId: shipment.statusEntryId ?? null,
    status: shipment.status ?? null,
    carrierName: shipment.carrierName ?? null,
    trackingNumbers: shipment.trackingNumbers ? [...shipment.trackingNumbers] : null,
    shippedAt: shipment.shippedAt ? shipment.shippedAt.toISOString() : null,
    deliveredAt: shipment.deliveredAt ? shipment.deliveredAt.toISOString() : null,
    weightValue: shipment.weightValue !== void 0 && shipment.weightValue !== null ? Number(shipment.weightValue) : null,
    weightUnit: shipment.weightUnit ?? null,
    declaredValueNet: shipment.declaredValueNet !== void 0 && shipment.declaredValueNet !== null ? Number(shipment.declaredValueNet) : null,
    declaredValueGross: shipment.declaredValueGross !== void 0 && shipment.declaredValueGross !== null ? Number(shipment.declaredValueGross) : null,
    currencyCode: shipment.currencyCode ?? null,
    notesText: shipment.notesText ?? null,
    metadata: shipment.metadata ? cloneJson(shipment.metadata) : null,
    customFields: customFields && Object.keys(customFields).length ? customFields : null,
    items: itemsSnapshot,
    itemsSnapshot
  };
}
async function restoreShipmentSnapshot(em, snapshot) {
  const order = await findOneWithDecryption(em, SalesOrder, { id: snapshot.orderId }, {}, { tenantId: snapshot.tenantId, organizationId: snapshot.organizationId });
  if (!order) return;
  const existing = await em.findOne(SalesShipment, { id: snapshot.id });
  const entity = existing ?? em.create(SalesShipment, {
    id: snapshot.id,
    createdAt: /* @__PURE__ */ new Date(),
    organizationId: snapshot.organizationId,
    tenantId: snapshot.tenantId
  });
  entity.order = order;
  entity.organizationId = snapshot.organizationId;
  entity.tenantId = snapshot.tenantId;
  entity.shipmentNumber = snapshot.shipmentNumber ?? null;
  entity.shippingMethodId = snapshot.shippingMethodId ?? null;
  entity.statusEntryId = snapshot.statusEntryId ?? null;
  entity.status = snapshot.status ?? null;
  entity.carrierName = snapshot.carrierName ?? null;
  entity.trackingNumbers = snapshot.trackingNumbers ? [...snapshot.trackingNumbers] : null;
  entity.shippedAt = snapshot.shippedAt ? new Date(snapshot.shippedAt) : null;
  entity.deliveredAt = snapshot.deliveredAt ? new Date(snapshot.deliveredAt) : null;
  entity.weightValue = snapshot.weightValue !== null ? snapshot.weightValue.toString() : null;
  entity.weightUnit = snapshot.weightUnit ?? null;
  entity.declaredValueNet = snapshot.declaredValueNet !== null ? snapshot.declaredValueNet.toString() : null;
  entity.declaredValueGross = snapshot.declaredValueGross !== null ? snapshot.declaredValueGross.toString() : null;
  entity.currencyCode = snapshot.currencyCode ?? null;
  entity.notesText = snapshot.notesText ?? null;
  entity.metadata = snapshot.metadata ? cloneJson(snapshot.metadata) : null;
  entity.updatedAt = /* @__PURE__ */ new Date();
  em.persist(entity);
  await em.flush();
  const existingItems = await em.find(SalesShipmentItem, { shipment: entity });
  existingItems.forEach((item) => em.remove(item));
  const items = Array.isArray(snapshot.items) ? snapshot.items : [];
  const lineIds = items.map((item) => item.orderLineId).filter((id) => typeof id === "string" && id.length > 0);
  const lines = lineIds.length ? await em.find(SalesOrderLine, { id: { $in: lineIds }, order: order.id }) : [];
  const lineMap = new Map(lines.map((line) => [line.id, line]));
  const restoredItems = [];
  items.forEach((item) => {
    const line = lineMap.get(item.orderLineId);
    if (!line) return;
    restoredItems.push(item);
    const lineRef = em.getReference(SalesOrderLine, line.id);
    const shipmentItem = em.create(SalesShipmentItem, {
      id: item.id ?? randomUUID(),
      shipment: entity,
      orderLine: lineRef,
      organizationId: snapshot.organizationId,
      tenantId: snapshot.tenantId,
      quantity: item.quantity.toString(),
      metadata: item.metadata ? cloneJson(item.metadata) : null
    });
    em.persist(shipmentItem);
  });
  const snapshotItems = (snapshot.itemsSnapshot ?? restoredItems).filter(
    (entry) => entry?.orderLineId ? lineMap.has(entry.orderLineId) : false
  );
  entity.itemsSnapshot = snapshotItems && snapshotItems.length ? cloneJson(snapshotItems) : null;
  if (!entity.itemsSnapshot) {
    await refreshShipmentItemsSnapshot(em, entity);
  }
  if (snapshot.customFields !== void 0) {
    await setRecordCustomFields(em, {
      entityId: E.sales.sales_shipment,
      recordId: entity.id,
      organizationId: snapshot.organizationId,
      tenantId: snapshot.tenantId,
      values: normalizeCustomFieldsInput(snapshot.customFields)
    });
  }
  em.persist(entity);
}
async function deleteShipmentWithItems(em, shipment) {
  const scope = { tenantId: shipment.tenantId, organizationId: shipment.organizationId };
  const items = await findWithDecryption(em, SalesShipmentItem, { shipment }, {}, scope);
  items.forEach((item) => em.remove(item));
  em.remove(shipment);
  await em.flush();
}
async function recomputeFulfilledQuantities(em, order) {
  const scope = { tenantId: order.tenantId, organizationId: order.organizationId };
  const shipments = await findWithDecryption(em, SalesShipment, { order, deletedAt: null }, {}, scope);
  const shipmentIds = shipments.map((entry) => entry.id);
  const shipmentItems = shipmentIds.length ? await findWithDecryption(em, SalesShipmentItem, { shipment: { $in: shipmentIds } }, {}, scope) : [];
  const totals = shipmentItems.reduce((acc, item) => {
    const lineId = typeof item.orderLine === "string" ? item.orderLine : item.orderLine?.id ?? item.orderLineId ?? null;
    if (!lineId) return acc;
    const next = (acc.get(lineId) ?? 0) + toNumber(item.quantity);
    acc.set(lineId, next);
    return acc;
  }, /* @__PURE__ */ new Map());
  const lines = await findWithDecryption(em, SalesOrderLine, { order }, { lockMode: LockMode.PESSIMISTIC_WRITE }, scope);
  lines.forEach((line) => {
    const shipped = totals.get(line.id) ?? 0;
    line.fulfilledQuantity = shipped.toString();
  });
}
async function loadOrder(em, id, scope) {
  const order = await findOneWithDecryption(em, SalesOrder, { id, deletedAt: null }, {}, scope);
  if (!order) throw new CrudHttpError(404, { error: "sales.shipments.not_found" });
  return order;
}
async function loadShippedTotals(em, order, excludeShipmentId) {
  const scope = { tenantId: order.tenantId, organizationId: order.organizationId };
  const shipments = await findWithDecryption(em, SalesShipment, { order, deletedAt: null }, {}, scope);
  const shipmentIds = shipments.map((entry) => entry.id).filter((id) => !excludeShipmentId || id !== excludeShipmentId);
  if (!shipmentIds.length) return /* @__PURE__ */ new Map();
  const items = await findWithDecryption(em, SalesShipmentItem, { shipment: { $in: shipmentIds } }, {}, scope);
  return items.reduce((acc, item) => {
    const lineId = typeof item.orderLine === "string" ? item.orderLine : item.orderLine?.id ?? item.orderLineId ?? null;
    if (!lineId) return acc;
    const next = (acc.get(lineId) ?? 0) + toNumber(item.quantity);
    acc.set(lineId, next);
    return acc;
  }, /* @__PURE__ */ new Map());
}
async function validateShipmentItems(params) {
  const { em, order, items, excludeShipmentId, lockOrderLines } = params;
  const { translate } = await resolveTranslations();
  if (!items || !items.length) {
    throw new CrudHttpError(400, { error: translate("sales.shipments.items_required", "Add at least one line to ship.") });
  }
  const scope = { tenantId: order.tenantId, organizationId: order.organizationId };
  const orderLines = await findWithDecryption(
    em,
    SalesOrderLine,
    { order },
    lockOrderLines ? { lockMode: LockMode.PESSIMISTIC_WRITE } : void 0,
    scope
  );
  const lineMap = new Map(orderLines.map((line) => [line.id, line]));
  const shippedTotals = await loadShippedTotals(em, order, excludeShipmentId);
  const requestedTotals = /* @__PURE__ */ new Map();
  for (const item of items) {
    const lineId = item.orderLineId;
    const quantity = toNumber(item.quantity);
    if (!lineId || quantity <= 0) {
      throw new CrudHttpError(400, { error: translate("sales.shipments.items_required", "Add at least one line to ship.") });
    }
    const line = lineMap.get(lineId);
    if (!line) {
      throw new CrudHttpError(404, { error: translate("sales.shipments.line_missing", "Order line not found.") });
    }
    const lineTotal = toNumber(line.quantity);
    const alreadyShipped = shippedTotals.get(lineId) ?? 0;
    const nextTotal = (requestedTotals.get(lineId) ?? 0) + quantity;
    if (nextTotal + alreadyShipped - 1e-6 > lineTotal) {
      throw new CrudHttpError(400, { error: translate("sales.shipments.quantity_exceeded", "Cannot ship more than the remaining quantity.") });
    }
    requestedTotals.set(lineId, nextTotal);
  }
  return {
    items: items.map((item) => ({
      orderLineId: item.orderLineId,
      quantity: toNumber(item.quantity),
      metadata: item.metadata ? cloneJson(item.metadata) : null
    })),
    lineMap
  };
}
function mergeAddressSnapshot(baseMetadata, snapshot) {
  const metadata = baseMetadata ? { ...baseMetadata } : {};
  if (snapshot && typeof snapshot === "object") {
    metadata[ADDRESS_SNAPSHOT_KEY] = cloneJson(snapshot);
  }
  return Object.keys(metadata).length ? metadata : null;
}
const createShipmentCommand = {
  id: "sales.shipments.create",
  async execute(rawInput, ctx) {
    const input = shipmentCreateSchema.parse(rawInput ?? {});
    ensureTenantScope(ctx, input.tenantId);
    ensureOrganizationScope(ctx, input.organizationId);
    const em = ctx.container.resolve("em").fork();
    const { translate } = await resolveTranslations();
    const shipment = await em.transactional(async (tx) => {
      const order = await loadOrder(tx, input.orderId, { tenantId: input.tenantId, organizationId: input.organizationId });
      ensureSameScope(order, input.organizationId, input.tenantId);
      await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
      const { items: normalizedItems, lineMap } = await validateShipmentItems({
        em: tx,
        order,
        items: input.items,
        lockOrderLines: true
      });
      const statusValue = await resolveDictionaryEntryValue(tx, input.statusEntryId ?? null, { tenantId: input.tenantId });
      const trackingNumbers = parseTrackingNumbers(input.trackingNumbers) ?? null;
      const metadata = mergeAddressSnapshot(
        input.metadata ? cloneJson(input.metadata) : null,
        input.shipmentAddressSnapshot ?? order.shippingAddressSnapshot ?? null
      ) ?? null;
      const shipmentId = randomUUID();
      const entity = tx.create(SalesShipment, {
        id: shipmentId,
        order,
        organizationId: input.organizationId,
        tenantId: input.tenantId,
        shipmentNumber: input.shipmentNumber ?? null,
        shippingMethodId: input.shippingMethodId ?? null,
        statusEntryId: input.statusEntryId ?? null,
        status: statusValue,
        carrierName: input.carrierName ?? null,
        trackingNumbers,
        shippedAt: input.shippedAt ?? null,
        deliveredAt: input.deliveredAt ?? null,
        weightValue: input.weightValue !== void 0 ? input.weightValue.toString() : null,
        weightUnit: input.weightUnit ?? null,
        declaredValueNet: input.declaredValueNet !== void 0 ? input.declaredValueNet.toString() : null,
        declaredValueGross: input.declaredValueGross !== void 0 ? input.declaredValueGross.toString() : null,
        currencyCode: input.currencyCode ?? order.currencyCode ?? null,
        notesText: input.notes ?? null,
        metadata,
        createdAt: /* @__PURE__ */ new Date(),
        updatedAt: /* @__PURE__ */ new Date()
      });
      const createdItems = [];
      normalizedItems.forEach((item) => {
        const lineRef = tx.getReference(SalesOrderLine, item.orderLineId);
        const shipmentItem = tx.create(SalesShipmentItem, {
          id: randomUUID(),
          shipment: entity,
          orderLine: lineRef,
          organizationId: input.organizationId,
          tenantId: input.tenantId,
          quantity: item.quantity.toString(),
          metadata: item.metadata ? cloneJson(item.metadata) : null
        });
        createdItems.push(shipmentItem);
        tx.persist(shipmentItem);
      });
      tx.persist(entity);
      if (input.customFields !== void 0) {
        await setRecordCustomFields(tx, {
          entityId: E.sales.sales_shipment,
          recordId: entity.id,
          organizationId: input.organizationId,
          tenantId: input.tenantId,
          values: normalizeCustomFieldsInput(input.customFields)
        });
      }
      if (input.documentStatusEntryId !== void 0) {
        const orderStatus = await resolveDictionaryEntryValue(tx, input.documentStatusEntryId ?? null, { tenantId: input.tenantId });
        if (input.documentStatusEntryId && !orderStatus) {
          throw new CrudHttpError(400, { error: translate("sales.documents.detail.statusInvalid", "Selected status could not be found.") });
        }
        order.statusEntryId = input.documentStatusEntryId ?? null;
        order.status = orderStatus;
        order.updatedAt = /* @__PURE__ */ new Date();
      }
      if (input.lineStatusEntryId !== void 0) {
        const lineStatus = await resolveDictionaryEntryValue(tx, input.lineStatusEntryId ?? null, { tenantId: input.tenantId });
        if (input.lineStatusEntryId && !lineStatus) {
          throw new CrudHttpError(400, { error: translate("sales.documents.detail.statusInvalid", "Selected status could not be found.") });
        }
        const uniqueLineIds = Array.from(new Set(normalizedItems.map((item) => item.orderLineId)));
        uniqueLineIds.forEach((lineId) => {
          const line = lineMap.get(lineId);
          if (!line) return;
          line.statusEntryId = input.lineStatusEntryId ?? null;
          line.status = lineStatus;
          line.updatedAt = /* @__PURE__ */ new Date();
        });
      }
      await refreshShipmentItemsSnapshot(tx, entity, { items: createdItems, lineMap });
      await tx.flush();
      await recomputeFulfilledQuantities(tx, order);
      await tx.flush();
      return entity;
    });
    const dataEngine = ctx.container.resolve("dataEngine");
    await emitCrudSideEffects({
      dataEngine,
      action: "created",
      entity: shipment,
      identifiers: {
        id: shipment.id,
        organizationId: shipment.organizationId,
        tenantId: shipment.tenantId
      },
      indexer: { entityType: E.sales.sales_shipment },
      events: shipmentCrudEvents
    });
    return { shipmentId: shipment.id };
  },
  captureAfter: async (_input, result, ctx) => {
    const em = ctx.container.resolve("em").fork();
    return loadShipmentSnapshot(em, result.shipmentId);
  },
  buildLog: async ({ result, snapshots }) => {
    const after = snapshots.after;
    if (!after) return null;
    const { translate } = await resolveTranslations();
    return {
      actionLabel: translate("sales.audit.shipments.create", "Create shipment"),
      resourceKind: "sales.shipment",
      resourceId: result.shipmentId,
      parentResourceKind: "sales.order",
      parentResourceId: after.orderId ?? null,
      tenantId: after.tenantId,
      organizationId: after.organizationId,
      snapshotAfter: after,
      payload: {
        undo: { after },
        __redoInput: buildShipmentCreateRedoInput(after)
      }
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const payload = extractUndoPayload(logEntry);
    const after = payload?.after;
    if (!after) return;
    const em = ctx.container.resolve("em").fork();
    await em.transactional(async (tx) => {
      const existing = await findOneWithDecryption(
        tx,
        SalesShipment,
        { id: after.id },
        { populate: ["order"] },
        { tenantId: after.tenantId, organizationId: after.organizationId }
      );
      if (!existing) return;
      const order = existing.order;
      await deleteShipmentWithItems(tx, existing);
      if (order) {
        await recomputeFulfilledQuantities(tx, order);
        await tx.flush();
      }
    });
  },
  redo: async ({ ctx, logEntry }) => {
    const after = resolveRedoSnapshot(logEntry);
    const shipmentId = after?.id ?? logEntry.resourceId ?? null;
    if (!after || !shipmentId) {
      throw new CrudHttpError(400, { error: "[internal] redo snapshot unavailable for sales.shipments.create" });
    }
    const em = ctx.container.resolve("em").fork();
    await em.transactional(async (tx) => {
      await restoreShipmentSnapshot(tx, after);
      const order = await findOneWithDecryption(tx, SalesOrder, { id: after.orderId }, {}, { tenantId: after.tenantId, organizationId: after.organizationId });
      await tx.flush();
      if (order) {
        await recomputeFulfilledQuantities(tx, order);
        await tx.flush();
      }
    });
    const shipment = await findOneWithDecryption(
      em,
      SalesShipment,
      { id: after.id },
      {},
      { tenantId: after.tenantId, organizationId: after.organizationId }
    );
    const dataEngine = ctx.container.resolve("dataEngine");
    await emitCrudSideEffects({
      dataEngine,
      action: "created",
      entity: shipment,
      identifiers: {
        id: after.id,
        organizationId: after.organizationId,
        tenantId: after.tenantId
      },
      indexer: { entityType: E.sales.sales_shipment },
      events: shipmentCrudEvents
    });
    return { shipmentId: after.id };
  }
};
const updateShipmentCommand = {
  id: "sales.shipments.update",
  async prepare(rawInput, ctx) {
    const parsed = shipmentUpdateSchema.parse(rawInput ?? {});
    if (!parsed.id) return {};
    const em = ctx.container.resolve("em");
    const snapshot = await loadShipmentSnapshot(em, parsed.id);
    if (snapshot) {
      ensureTenantScope(ctx, snapshot.tenantId);
      ensureOrganizationScope(ctx, snapshot.organizationId);
    }
    return snapshot ? { before: snapshot } : {};
  },
  async execute(rawInput, ctx) {
    const input = shipmentUpdateSchema.parse(rawInput ?? {});
    ensureTenantScope(ctx, input.tenantId);
    ensureOrganizationScope(ctx, input.organizationId);
    const em = ctx.container.resolve("em").fork();
    const { translate } = await resolveTranslations();
    const shipment = await em.transactional(async (tx) => {
      const shipmentEntity = await findOneWithDecryption(
        tx,
        SalesShipment,
        { id: input.id },
        { populate: ["order"] },
        { tenantId: input.tenantId, organizationId: input.organizationId }
      );
      if (!shipmentEntity || !shipmentEntity.order) {
        throw new CrudHttpError(404, { error: "sales.shipments.not_found" });
      }
      ensureSameScope(shipmentEntity, input.organizationId, input.tenantId);
      const order = shipmentEntity.order;
      if (input.orderId && input.orderId !== order.id) {
        throw new CrudHttpError(400, { error: "sales.shipments.invalid_order" });
      }
      await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
      const paidAmount = parseFloat(order.paidTotalAmount || "0");
      const refundedAmount = parseFloat(order.refundedTotalAmount || "0");
      const grandTotal = parseFloat(order.grandTotalGrossAmount || "0");
      const tolerance = 1e-4;
      const isFullyRefunded = refundedAmount >= grandTotal - tolerance;
      if (isFullyRefunded) {
        throw new CrudHttpError(422, { error: translate("sales.shipments.fully_returned", "Cannot modify shipment: order is fully returned") });
      }
      const isFullyPaid = paidAmount >= grandTotal - tolerance;
      if (isFullyPaid) {
        throw new CrudHttpError(422, { error: translate("sales.shipments.payment_completed", "Cannot modify shipment: order payment is completed") });
      }
      const validatedItems = input.items ? await validateShipmentItems({
        em: tx,
        order,
        items: input.items,
        excludeShipmentId: shipmentEntity.id
      }) : null;
      const normalizedItems = validatedItems?.items ?? null;
      const lineMap = validatedItems?.lineMap ?? /* @__PURE__ */ new Map();
      const resolvedShipmentStatus = input.statusEntryId !== void 0 ? await resolveDictionaryEntryValue(tx, input.statusEntryId ?? null, { tenantId: shipmentEntity.tenantId }) : void 0;
      if (input.shipmentNumber !== void 0) shipmentEntity.shipmentNumber = input.shipmentNumber ?? null;
      if (input.shippingMethodId !== void 0) shipmentEntity.shippingMethodId = input.shippingMethodId ?? null;
      if (input.statusEntryId !== void 0) {
        shipmentEntity.statusEntryId = input.statusEntryId ?? null;
        shipmentEntity.status = resolvedShipmentStatus ?? null;
      }
      if (input.carrierName !== void 0) shipmentEntity.carrierName = input.carrierName ?? null;
      if (input.trackingNumbers !== void 0) shipmentEntity.trackingNumbers = parseTrackingNumbers(input.trackingNumbers);
      if (input.shippedAt !== void 0) shipmentEntity.shippedAt = input.shippedAt ?? null;
      if (input.deliveredAt !== void 0) shipmentEntity.deliveredAt = input.deliveredAt ?? null;
      if (input.weightValue !== void 0) shipmentEntity.weightValue = input.weightValue !== null ? input.weightValue.toString() : null;
      if (input.weightUnit !== void 0) shipmentEntity.weightUnit = input.weightUnit ?? null;
      if (input.declaredValueNet !== void 0) {
        shipmentEntity.declaredValueNet = input.declaredValueNet !== null ? input.declaredValueNet.toString() : null;
      }
      if (input.declaredValueGross !== void 0) {
        shipmentEntity.declaredValueGross = input.declaredValueGross !== null ? input.declaredValueGross.toString() : null;
      }
      if (input.currencyCode !== void 0) shipmentEntity.currencyCode = input.currencyCode ?? null;
      if (input.notes !== void 0) shipmentEntity.notesText = input.notes ?? null;
      if (input.metadata !== void 0 || input.shipmentAddressSnapshot !== void 0) {
        shipmentEntity.metadata = mergeAddressSnapshot(
          input.metadata ? cloneJson(input.metadata) : shipmentEntity.metadata ?? null,
          input.shipmentAddressSnapshot
        );
      }
      shipmentEntity.updatedAt = /* @__PURE__ */ new Date();
      await tx.flush();
      const shouldLoadItems = Boolean(normalizedItems || input.lineStatusEntryId !== void 0);
      const existingItems = shouldLoadItems ? await findWithDecryption(tx, SalesShipmentItem, { shipment: shipmentEntity }, {}, { tenantId: shipmentEntity.tenantId, organizationId: shipmentEntity.organizationId }) : [];
      const newItems = [];
      if (normalizedItems) {
        existingItems.forEach((item) => tx.remove(item));
        normalizedItems.forEach((item) => {
          const lineRef = tx.getReference(SalesOrderLine, item.orderLineId);
          const shipmentItem = tx.create(SalesShipmentItem, {
            id: randomUUID(),
            shipment: shipmentEntity,
            orderLine: lineRef,
            organizationId: shipmentEntity.organizationId,
            tenantId: shipmentEntity.tenantId,
            quantity: item.quantity.toString(),
            metadata: item.metadata ? cloneJson(item.metadata) : null
          });
          newItems.push(shipmentItem);
          tx.persist(shipmentItem);
        });
      }
      if (input.customFields !== void 0) {
        await setRecordCustomFields(tx, {
          entityId: E.sales.sales_shipment,
          recordId: shipmentEntity.id,
          organizationId: shipmentEntity.organizationId,
          tenantId: shipmentEntity.tenantId,
          values: normalizeCustomFieldsInput(input.customFields)
        });
      }
      if (input.documentStatusEntryId !== void 0) {
        const orderStatus = await resolveDictionaryEntryValue(tx, input.documentStatusEntryId ?? null, { tenantId: shipmentEntity.tenantId });
        if (input.documentStatusEntryId && !orderStatus) {
          throw new CrudHttpError(400, { error: translate("sales.documents.detail.statusInvalid", "Selected status could not be found.") });
        }
        order.statusEntryId = input.documentStatusEntryId ?? null;
        order.status = orderStatus;
        order.updatedAt = /* @__PURE__ */ new Date();
      }
      if (input.lineStatusEntryId !== void 0) {
        const lineStatus = await resolveDictionaryEntryValue(tx, input.lineStatusEntryId ?? null, { tenantId: shipmentEntity.tenantId });
        if (input.lineStatusEntryId && !lineStatus) {
          throw new CrudHttpError(400, { error: translate("sales.documents.detail.statusInvalid", "Selected status could not be found.") });
        }
        const targetLineIds = normalizedItems ? Array.from(new Set(normalizedItems.map((item) => item.orderLineId))) : Array.from(
          new Set(
            existingItems.map(
              (item) => typeof item.orderLine === "string" ? item.orderLine : item.orderLine?.id ?? null
            ).filter((id) => Boolean(id))
          )
        );
        if (targetLineIds.length) {
          const missing = targetLineIds.filter((id) => !lineMap.has(id));
          if (missing.length) {
            const fetched = await tx.find(SalesOrderLine, { id: { $in: missing } });
            fetched.forEach((line) => lineMap.set(line.id, line));
          }
          targetLineIds.forEach((lineId) => {
            const line = lineMap.get(lineId);
            if (!line) return;
            line.statusEntryId = input.lineStatusEntryId ?? null;
            line.status = lineStatus;
            line.updatedAt = /* @__PURE__ */ new Date();
          });
        }
      }
      await tx.flush();
      const itemsForSnapshot = normalizedItems || shouldLoadItems ? normalizedItems ? newItems : existingItems : await findWithDecryption(tx, SalesShipmentItem, { shipment: shipmentEntity }, {}, { tenantId: shipmentEntity.tenantId, organizationId: shipmentEntity.organizationId });
      await refreshShipmentItemsSnapshot(tx, shipmentEntity, { items: itemsForSnapshot, lineMap });
      await tx.flush();
      await recomputeFulfilledQuantities(tx, order);
      await tx.flush();
      return shipmentEntity;
    });
    const dataEngine = ctx.container.resolve("dataEngine");
    await emitCrudSideEffects({
      dataEngine,
      action: "updated",
      entity: shipment,
      identifiers: {
        id: shipment.id,
        organizationId: shipment.organizationId,
        tenantId: shipment.tenantId
      },
      indexer: { entityType: E.sales.sales_shipment },
      events: shipmentCrudEvents
    });
    return { shipmentId: shipment.id };
  },
  captureAfter: async (_input, result, ctx) => {
    const em = ctx.container.resolve("em").fork();
    return loadShipmentSnapshot(em, result.shipmentId);
  },
  buildLog: async ({ snapshots, result }) => {
    const { translate } = await resolveTranslations();
    const before = snapshots.before;
    const after = snapshots.after;
    return {
      actionLabel: translate("sales.audit.shipments.update", "Update shipment"),
      resourceKind: "sales.shipment",
      resourceId: result.shipmentId,
      parentResourceKind: "sales.order",
      parentResourceId: after?.orderId ?? before?.orderId ?? null,
      tenantId: after?.tenantId ?? before?.tenantId ?? null,
      organizationId: after?.organizationId ?? before?.organizationId ?? null,
      snapshotBefore: before ?? null,
      snapshotAfter: after ?? null,
      payload: {
        undo: { before, after }
      }
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const payload = extractUndoPayload(logEntry);
    const before = payload?.before;
    if (!before) return;
    const em = ctx.container.resolve("em").fork();
    await em.transactional(async (tx) => {
      await restoreShipmentSnapshot(tx, before);
      const order = await findOneWithDecryption(tx, SalesOrder, { id: before.orderId }, {}, { tenantId: before.tenantId, organizationId: before.organizationId });
      await tx.flush();
      if (order) {
        await recomputeFulfilledQuantities(tx, order);
        await tx.flush();
      }
    });
  }
};
const deleteShipmentCommand = {
  id: "sales.shipments.delete",
  async prepare(rawInput, ctx) {
    const parsed = shipmentUpdateSchema.parse(rawInput ?? {});
    if (!parsed.id) return {};
    const em = ctx.container.resolve("em");
    const snapshot = await loadShipmentSnapshot(em, parsed.id);
    if (snapshot) {
      try {
        ensureTenantScope(ctx, snapshot.tenantId);
      } catch (error) {
        logShipmentDeleteScopeRejection(ctx, "Tenant mismatch while preparing shipment delete", {
          shipmentId: snapshot.id,
          snapshotTenantId: snapshot.tenantId,
          authTenantId: ctx.auth?.tenantId ?? null
        });
        throw error;
      }
      try {
        ensureOrganizationScope(ctx, snapshot.organizationId);
      } catch (error) {
        logShipmentDeleteScopeRejection(ctx, "Organization mismatch while preparing shipment delete", {
          shipmentId: snapshot.id,
          snapshotOrganizationId: snapshot.organizationId,
          selectedOrganizationId: ctx.selectedOrganizationId ?? null,
          authOrganizationId: ctx.auth?.orgId ?? null
        });
        throw error;
      }
    }
    return snapshot ? { before: snapshot } : {};
  },
  async execute(rawInput, ctx) {
    const { translate } = await resolveTranslations();
    const payload = shipmentUpdateSchema.pick({ id: true, orderId: true, organizationId: true, tenantId: true }).parse(rawInput ?? {});
    try {
      ensureTenantScope(ctx, payload.tenantId);
    } catch (error) {
      logShipmentDeleteScopeRejection(ctx, "Tenant mismatch while executing shipment delete", {
        shipmentId: payload.id,
        payloadTenantId: payload.tenantId,
        authTenantId: ctx.auth?.tenantId ?? null
      });
      throw error;
    }
    try {
      ensureOrganizationScope(ctx, payload.organizationId);
    } catch (error) {
      logShipmentDeleteScopeRejection(ctx, "Organization mismatch while executing shipment delete", {
        shipmentId: payload.id,
        payloadOrganizationId: payload.organizationId,
        selectedOrganizationId: ctx.selectedOrganizationId ?? null,
        authOrganizationId: ctx.auth?.orgId ?? null
      });
      throw error;
    }
    const em = ctx.container.resolve("em").fork();
    const { shipment, shipmentItems } = await em.transactional(async (tx) => {
      const shipmentEntity = await findOneWithDecryption(
        tx,
        SalesShipment,
        { id: payload.id },
        { populate: ["order"] },
        { tenantId: payload.tenantId, organizationId: payload.organizationId }
      );
      if (!shipmentEntity || !shipmentEntity.order) {
        throw new CrudHttpError(404, { error: translate("sales.shipments.not_found", "Shipment not found") });
      }
      try {
        ensureSameScope(shipmentEntity, payload.organizationId, payload.tenantId);
      } catch (error) {
        logShipmentDeleteScopeRejection(ctx, "Shipment scope mismatch against payload", {
          shipmentId: payload.id,
          shipmentOrganizationId: shipmentEntity.organizationId,
          shipmentTenantId: shipmentEntity.tenantId,
          payloadOrganizationId: payload.organizationId,
          payloadTenantId: payload.tenantId
        });
        throw error;
      }
      const order = shipmentEntity.order;
      if (order.id !== payload.orderId) {
        throw new CrudHttpError(400, { error: translate("sales.shipments.invalid_order", "Shipment does not belong to this order") });
      }
      await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER);
      const scope = { tenantId: shipmentEntity.tenantId, organizationId: shipmentEntity.organizationId };
      const items = await findWithDecryption(tx, SalesShipmentItem, { shipment: shipmentEntity }, {}, scope);
      items.forEach((item) => tx.remove(item));
      tx.remove(shipmentEntity);
      await tx.flush();
      await recomputeFulfilledQuantities(tx, order);
      await tx.flush();
      return { shipment: shipmentEntity, shipmentItems: items };
    });
    const dataEngine = ctx.container.resolve("dataEngine");
    await emitCrudSideEffects({
      dataEngine,
      action: "deleted",
      entity: shipment,
      identifiers: {
        id: shipment.id,
        organizationId: shipment.organizationId,
        tenantId: shipment.tenantId
      },
      indexer: { entityType: E.sales.sales_shipment },
      events: shipmentCrudEvents
    });
    if (shipmentItems.length) {
      await Promise.all(
        shipmentItems.map(
          (item) => emitCrudSideEffects({
            dataEngine,
            action: "deleted",
            entity: item,
            identifiers: {
              id: item.id,
              organizationId: item.organizationId ?? null,
              tenantId: item.tenantId ?? null
            },
            indexer: { entityType: E.sales.sales_shipment_item }
          })
        )
      );
    }
    return { shipmentId: shipment.id };
  },
  undo: async ({ logEntry, ctx }) => {
    const payload = extractUndoPayload(logEntry);
    const snapshot = payload?.before ?? null;
    if (!snapshot) return;
    const em = ctx.container.resolve("em").fork();
    await em.transactional(async (tx) => {
      await restoreShipmentSnapshot(tx, snapshot);
      const order = await findOneWithDecryption(tx, SalesOrder, { id: snapshot.orderId }, {}, { tenantId: snapshot.tenantId, organizationId: snapshot.organizationId });
      await tx.flush();
      if (order) {
        await recomputeFulfilledQuantities(tx, order);
        await tx.flush();
      }
    });
  },
  buildLog: async ({ snapshots }) => {
    const before = snapshots.before;
    if (!before) return null;
    const { translate } = await resolveTranslations();
    return {
      actionLabel: translate("sales.audit.shipments.delete", "Delete shipment"),
      resourceKind: "sales.shipment",
      resourceId: before.id ?? null,
      parentResourceKind: "sales.order",
      parentResourceId: before.orderId ?? null,
      tenantId: before.tenantId,
      organizationId: before.organizationId,
      snapshotBefore: before,
      payload: {
        undo: { before }
      }
    };
  }
};
registerCommand(createShipmentCommand);
registerCommand(updateShipmentCommand);
registerCommand(deleteShipmentCommand);
const shipmentCommands = [createShipmentCommand, updateShipmentCommand, deleteShipmentCommand];
export {
  loadShipmentSnapshot,
  restoreShipmentSnapshot,
  shipmentCommands
};
//# sourceMappingURL=shipments.js.map
