# `PhoenixKitWarehouse.Transfers`
[🔗](https://github.com/BeamLabEU/phoenix_kit_warehouse/blob/0.4.0/lib/phoenix_kit_warehouse/transfers.ex#L1)

Context for managing transfers (stock moved between two warehouses).

A transfer moves stock from a `source_location_uuid` warehouse to a
`destination_location_uuid` warehouse via two separate atomic postings —
not one shared transaction — because the goods physically leave the
source and arrive at the destination at different points in time:

  * `ship_transfer/2` (draft -> in_transit) DECREASES stock at the source
    (conditional decrement — the whole Multi rolls back if any line has
    insufficient stock, mirroring `GoodsIssues.post_goods_issue/2`).
  * `receive_transfer/2` (in_transit -> done) INCREASES stock at the
    destination (additive delta, mirroring
    `GoodsReceipts.post_goods_receipt/2`).

Both locations must be chosen and distinct before a transfer can ship —
`ship_transfer/2` and `receive_transfer/2` return `{:error,
:locations_required}` up front rather than silently falling back to the
configured default warehouse.

A transfer can also be cancelled via `cancel_transfer/2`, from `draft`
(no postings — nothing moved yet) or from `in_transit` (credits stock back
to the source, reversing `ship_transfer/2`). It cannot be cancelled once
`done` (received) or already `cancelled`.

# `add_source_ref`

Manually attaches a traceability reference to a transfer.

`type` must be a kind registered via `PhoenixKitWarehouse.SourceKinds`.
Pure metadata — does not touch `lines` and is not gated to draft status.
A duplicate `{type, uuid}` pair is a no-op.

# `cancel_transfer`

Cancels a transfer.

- From `draft`: NO stock postings — the goods never physically moved, so
  cancelling just locks the row FOR UPDATE (re-checking status == "draft",
  guarding against a concurrent ship) and flips status -> "cancelled" via
  `Transfer.cancel_changeset/2`.
- From `in_transit`: reverses the `ship_transfer/2` posting. Locks the row
  FOR UPDATE (re-checking status == "in_transit") and, for each line with
  transfer_quantity > 0, credits the quantity BACK to `source_location_uuid`
  via `StockLedger.receive_quantity/3` (additive — unlike issuing, this
  cannot fail on insufficient stock). Captures `reversed_source_quantity`
  (the source's on-hand quantity immediately before the credit) on each
  line for audit, mirroring `previous_source_quantity`/
  `previous_destination_quantity` on the other two legs. Does not touch
  the destination — nothing arrived there yet.
- From `done` or already `cancelled`: returns `{:error, :not_cancellable}`
  — a completed transfer can't be un-received, and a cancelled transfer
  can't be cancelled twice.

# `correct_transfer`

Corrects the note and/or storage_folder_uuid of a transfer without
changing status or lines. Works on documents in any status.

# `create_transfer`

Creates a new draft transfer.

Unlike `GoodsIssues.create_goods_issue/1` and friends, `source_location_uuid`
and `destination_location_uuid` do NOT default to the configured default
warehouse — a transfer is meaningless without two *specific*, distinct
warehouses, so both are left `nil` when not supplied in `attrs`. The UI
requires both to be chosen before the transfer can be shipped (see
`ship_transfer/2`).

`created_by_uuid` is set programmatically — not via cast.

# `get_transfer`

Returns `{:ok, transfer}` or `{:error, :not_found}`.

# `get_transfer!`

Returns the transfer or raises.

# `list_transfers`

Lists non-deleted transfers ordered by number descending (newest first).

# `receive_transfer`

Receives a transfer in an `Ecto.Multi` transaction (in_transit -> done).
INCREASES stock at `destination_location_uuid`.

- Returns `{:error, :locations_required}` when either location is `nil`
  or they're equal to each other. Both are guaranteed to already be set at
  this stage (the transfer went through `ship_transfer/2` first), but the
  check is cheap and guards against manually-corrupted data.
- Locks the row FOR UPDATE and re-checks status == "in_transit" (prevents
  double-receiving).
- Deduplicates lines by item_uuid.
- For each line with transfer_quantity > 0:
  - Captures `previous_destination_quantity` = current on-hand at the
    destination for audit.
  - Calls `StockLedger.receive_quantity/3` (additive stock delta — does
    NOT touch the source again).
- Lines with transfer_quantity == 0 contribute no stock change.
- Flips status -> "done", sets received_at and performed_by_uuid.

Returns `{:error, :not_in_transit}` for transfers not in `in_transit` status.

# `remove_source_ref`

Detaches a traceability reference from a transfer. No-op when the
`{type, uuid}` pair isn't present.

# `set_storage_folder`

Sets the `storage_folder_uuid` on a transfer. Works on documents in any status.

# `ship_transfer`

Ships a transfer in an `Ecto.Multi` transaction (draft -> in_transit).
DECREASES stock at `source_location_uuid`.

- Returns `{:error, :locations_required}` BEFORE touching the database
  when `source_location_uuid` or `destination_location_uuid` is `nil`, or
  when they're equal to each other — `StockLedger.issue_quantity/3` would
  otherwise silently fall back to the configured default warehouse for a
  `nil` location, a materially different (and wrong) outcome from "no
  location chosen yet".
- Locks the row FOR UPDATE and re-checks status == "draft" (prevents
  double-shipping).
- Deduplicates lines by item_uuid.
- For each line with transfer_quantity > 0:
  - Captures `previous_source_quantity` = current on-hand at the source
    for audit.
  - Calls `StockLedger.issue_quantity/3` (conditional decrement).
  - If ANY line returns `{:error, {:insufficient_stock, _}}`, the WHOLE
    Multi rolls back: stock is unchanged and the document stays draft.
- Lines with transfer_quantity == 0 contribute no stock change.
- Flips status -> "in_transit", sets shipped_at and performed_by_uuid.

Returns `{:error, :not_draft}` for non-draft transfers.

# `soft_delete_transfer`

Soft-deletes a draft transfer. Returns {:error, :not_draft} for shipped/received/cancelled transfers.

# `update_draft`

Updates a draft transfer. Returns `{:error, :not_draft}` when not in draft
status. Locations may be left/set to `nil` — see `create_transfer/1`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
