# Source module → Operations Projection example

A source module does not call an arbitrary Exception Center REST endpoint from its domain transaction. Prefer an Outbox event and an idempotent consumer that updates the operational projection.

Example flow:

```text
OMS AllocationFailed
  ↓ same OMS transaction
Outbox
  ↓
Operations projection consumer
  ↓
OperationsProjectionWriter.UpsertAsync
  ↓
COMMON-OPS-001
```

When the source condition becomes normal:

```text
OMS AllocationSucceeded
  ↓
Outbox
  ↓
OperationsProjectionWriter.ResolveBySourceAsync
```

Suggested mapping:

```csharp
await writer.UpsertAsync(new UpsertWorkItem(
    tenantId,
    "OMS",
    "order",
    orderId.ToString(),
    orderNo,
    "OMS-ORD-001",
    orderVersion,
    "STOCK_SHORTAGE",
    $"재고 부족 · {orderNo}",
    "출고 가능한 재고가 부족합니다.",
    "warning",
    occurredAt,
    dueAt: shipmentCutoff,
    context: new { orderNo, shortageSkuCount }), ct);
```

Recovery:

```csharp
await writer.ResolveBySourceAsync(
    tenantId,
    "OMS",
    "order",
    orderId.ToString(),
    "STOCK_SHORTAGE",
    "재고 할당 성공",
    orderVersion,
    ct);
```

This keeps the source module authoritative and makes the exception center disposable/rebuildable as a projection.


## Out-of-order event protection

`source_version` is part of the projection contract. An older or duplicate failure event must not reopen an exception after a newer recovery event has resolved it. Producers should therefore publish a monotonically increasing aggregate/source version whenever possible.
