# Command Implementation

## Unified Pattern: `run` + generated shell

All commands follow the same pattern — export a `run` function, and the generated shell wraps it with `defineCommand`:

**Implementation file** (`command/myCommand.ts`):

```typescript
export async function run(db: DB, input: MyCommandInput, ctx: CommandContext) {
  // validate → query → mutate
  return ok({ entity });
}
```

**Generated shell** (`command/myCommand.generated.ts`):

```typescript
export const myCommand = defineCommand("myCommand", permissions.myScope.myCommand, run);
```

## Custom Fields (generic CF)

Commands that **write** (insert or update) into a table with user-extensible fields make `run` generic:

- Generic `CF extends Record<string, unknown>` on the `run` function
- Input type: `CreateXInput & CF` or `UpdateXInput & Partial<CF>`
- Destructure known fields, rest-spread custom fields
- Cast custom fields: `...(customFields as Record<string, unknown>)`

```typescript
export async function run<CF extends Record<string, unknown>>(
  db: DB,
  input: CreateXInput & CF,
  ctx: CommandContext,
) {
  const { name, ...customFields } = input;
  await db
    .insertInto("X")
    .values({ ...(customFields as Record<string, unknown>), name })
    .execute();
}
```

### module.ts wiring

Instantiate the command directly in `module.ts` and publish the instantiated function from `commands`:

```typescript
import type { TailorDBInsertable } from "@tailor-platform/sdk/kysely";

return {
  commands: {
    createX: createX<TailorDBInsertable<F>>(),
  },
};
```

`TailorDBInsertable<F>` turns the extension-field map into the create input: a
`.serial()` field is never caller-supplied, `.default()` / `.hooks({ create })` ones may
be omitted, and optional ones stay optional.

### Rule: when to use generic CF

> If a model has a `fields` custom-field param, **every command that writes those fields** must have a generic `CF`. This includes create, update, and any other write command. The determining factor is whether the command writes to a table with custom fields, not just whether it inserts new rows.

### Line-level custom fields (header + line CF)

Commands that write both a header table and a line table with custom fields use **two generics** — `CF` for the header and `LCF` for lines:

- Signature: `run<CF, LCF>` with both `Record<string, unknown>` constraints
- Input type uses `Omit` to replace the typed `lines` array with one that accepts `LCF`:
  - Create: `Omit<CreateXInput, "lines"> & CF & { lines: (XLineInput & LCF)[] }`
  - Update: `Omit<UpdateXInput, "lines"> & Partial<CF> & { lines?: (XLineInput & LCF)[] }`
- Header: destructure known fields, rest-spread `customFields` into insert/update
- Lines: destructure known fields per line, rest-spread `lineCustomFields` into insert
- Create line inputs should **not** include `id?: string` — IDs are generated on insert
- Update line inputs may include `id?: string` for edit tracking

**Create example (header + lines):**

```typescript
export async function run<CF extends Record<string, unknown>, LCF extends Record<string, unknown>>(
  db: DB,
  input: Omit<CreateOrderInput, "lines"> & CF & { lines: (OrderLineInput & LCF)[] },
  ctx: CommandContext,
) {
  const { companyId, lines, ...customFields } = input;

  const order = await db
    .insertInto("Order")
    .values({ ...(customFields as Record<string, unknown>), companyId })
    .returningAll()
    .executeTakeFirst();

  await db
    .insertInto("OrderLine")
    .values(
      lines.map((line) => {
        const { itemId, quantity, ...lineCustomFields } = line;
        return {
          ...(lineCustomFields as Record<string, unknown>),
          orderId: order!.id,
          itemId,
          quantity,
        };
      }),
    )
    .execute();

  return ok({ order: order! });
}
```

## Config-Taking Commands

Commands that need external config (cross-module queries, settings) accept config as leading params before `(db, input, ctx)`:

```typescript
import type { PrimitivesQueries } from "../module";

export async function run<CF extends Record<string, unknown>>(
  primitivesQueries: Pick<PrimitivesQueries, "getUnit">,
  db: DB, input: CreateItemInput & CF, ctx: CommandContext,
) { ... }
```

Publish commands by calling the generated export in `module.ts`. Pass deps first if the command takes injected functions:

```typescript
return {
  commands: {
    createItem: createItem<TailorDBInsertable<IF>>(queries),
    deactivateItem: deactivateItem(),
  },
};
```

## Command-Side Reads (CQRS Separation)

Following CQRS principles, commands own their reads for same-module data. Cross-module reads use injected query functions to preserve module boundaries.

### Why

- **Query functions** use `ReadonlyDB` — shaped for API consumers, can't `forUpdate()`
- **Command reads** are shaped for enforcing invariants — need locking, may need different filters or joins
- **Independence** — query-side changes (pagination, field selection) must not affect command behavior
- **Module composition** — modules nest at multiple depths; parent modules inject child queries to maintain explicit dependency contracts

### Rule

> **Same-module reads**: Commands inline their own `selectFrom()` calls using `db` (full `DB`). Never import or call query functions from `query/` within the same module — the command owns its reads and can apply locking.
>
> **Cross-module reads**: Commands receive query functions from other modules via dependency injection (leading params). This keeps module boundaries explicit and supports nested module composition.
>
> **Status preconditions**: Commands enforce status checks inline (e.g., "only DRAFT items can be deleted", "only ACTIVE users can be assigned roles"). Status filtering belongs to the command's invariant logic, not to query-side defaults. See [Status-Aware Query Rules](queries.md#status-aware-query-rules) for the read-side conventions.

### Examples

**Correct** — same-module read inlined with locking:

```typescript
// command/deactivateUnit.ts
const unit = await db
  .selectFrom("Unit")
  .selectAll()
  .where("id", "=", input.unitId)
  .forUpdate()
  .executeTakeFirst();
if (!unit) return err(new UnitNotFoundError(input.unitId));
```

**Correct** — cross-module read via injected query:

```typescript
// command/createItem.ts (item-management validating a primitives entity)
export async function run<CF extends Record<string, unknown>>(
  primitivesQueries: Pick<PrimitivesQueries, "getUnit">,
  db: DB,
  input: CreateItemInput & CF,
  ctx: CommandContext,
) {
  const { unit } = await primitivesQueries.getUnit(db, { id: input.unitId }, ctx);
  if (!unit?.isActive) return err(new UnitNotFoundError(input.unitId));
  // ...
}
```

**Wrong** — calling same-module query function:

```typescript
// command/deactivateUnit.ts
import { getUnit } from "../query/getUnit"; // ← don't do this
const unit = await getUnit(db, { id: input.unitId }, ctx);
```

## Result Checking

Commands and queries return `Result`. Whether you need to check `.ok` depends on the return type:

- **Commands** (via `defineCommand`) always include a permission check, so the return type is a union that can be `{ ok: false }`. Callers **must** check `result.ok`.
- **Queries without permission** (via `defineQuery(name, run)`) return an ok-only type (`{ ok: true; value: T }`). Callers can access `.value` directly — no `.ok` check needed.
- **Queries with permission** (via `defineQuery(name, perm, run)`) can return `{ ok: false }`. Callers **must** check `result.ok`.

### Rule

> **Check `result.ok`** after calling a command or permission-gated query. If the result is an error, wrap it in a **caller-side domain error** and return `err(...)`. For ok-only queries, access `.value` directly.

### How to add the error

1. Add an error scenario to the command's doc (e.g. `- **STOCK_MOVEMENT_CREATION_FAILED**: Downstream stock movement creation failed during posting`)
2. Regenerate: `npx erp-kit module generate code -p <module-path>`
3. Import the generated error class and use it in the `if (!result.ok)` branch

### Examples

Command result — check `.ok`:

```typescript
const stockMoveResult = await inventoryCommands.createStockMovement(
  db,
  { movementType: "GOODS_RECEIPT", lines },
  ctx,
);
if (!stockMoveResult.ok) {
  return err(new StockMovementCreationFailedError(input.id));
}
```

Ok-only query result — access `.value` directly:

```typescript
const { item } = (await itemManagementQueries.getItem(db, { id: itemId }, ctx)).value;
if (!item) {
  return err(new ItemNotFoundError(itemId));
}
```

## Select Locking

Commands that read then write must lock rows with `forUpdate()` to prevent concurrent requests from causing race conditions.

### When to lock

| Pattern                                                     | Lock? | Example                                                                               |
| ----------------------------------------------------------- | ----- | ------------------------------------------------------------------------------------- |
| Read a record, then update/delete it                        | Yes   | `deactivateUnit`: reads unit, then sets `isActive = false`                            |
| Read a record to check uniqueness before insert             | Yes   | `createUnit`: checks symbol uniqueness, then inserts                                  |
| Read multiple related records, then update them             | Yes   | `setReferenceUnit`: reads all units in category, recalculates factors                 |
| Read a record only for validation (no write to that record) | No    | Reading a category to verify it exists before inserting a unit into a different table |
| Insert-only with no prior read                              | No    | `logAuditEvent`: pure insert                                                          |

### Rule

> Lock a `SELECT` when the command **writes to the same record it reads**, or when it **reads to enforce a uniqueness constraint** before inserting. The lock scope should be the narrowest set of rows needed — lock only the rows that participate in the read-then-write cycle.

### Code pattern

Chain `.forUpdate()` on the select query:

```typescript
const unit = await db
  .selectFrom("Unit")
  .selectAll()
  .where("id", "=", input.unitId)
  .forUpdate()
  .executeTakeFirst();
```

### Batch locking

When a command reads multiple rows then updates them (e.g., `setReferenceUnit`), lock the entire set:

```typescript
const units = await db
  .selectFrom("Unit")
  .selectAll()
  .where("categoryId", "=", input.categoryId)
  .forUpdate()
  .execute();
```

## Implementation Considerations

- **Error handling**: Use `ok()` / `err()` from `@tailor-platform/erp-kit/core` — do not throw
- **Validation**: Check referenced entities exist before operating, return `err()` if not found
- **Return property naming**: Use the full model name in camelCase as the return property key — never abbreviate. This keeps property names unambiguous across modules.
  - `return ok({ stockMovement: ... })` — not `movement`
  - `return ok({ inventoryAdjustment: ... })` — not `adjustment`

## Update Command Input Type

Update commands use a structured type that separates lookup keys from mutable fields, and explicitly excludes status-controlled fields.

### Input type shape

```typescript
export type UpdateXInput = ({
  id: string;
} | {
  naturalKey: string; // e.g. sku, code
}) & {
  mutableField1?: string;
  mutableField2?: string | null;
}
```

### Run signature with custom fields

```typescript
export async function run<CF extends Record<string, unknown>>(
  db: Transaction,
  input: UpdateXInput & Omit<Partial<CF>, "status">,
  ctx: CommandContext,
)
```

### Rules

| Field kind | Examples | How to handle |
|---|---|---|
| Lookup key (primary) | `id` | One branch of the union |
| Lookup key (natural) | `sku`, `code` | Another branch of the union — used to look up the record, not to update it |
| Mutable fields | `name`, `barcode` | Listed in the `&` intersection as optional fields |
| Status-controlled | `status` | **Never in the input type.** Managed by dedicated commands (`activateX`, `deactivateX`). Excluded from `CF` via `Omit`. |
| System fields | `createdAt`, `updatedAt` | Never in the input type. Set automatically. |

### Lookup implementation

Use `"id" in input` to branch between lookup strategies:

```typescript
let query = db.selectFrom("Item").selectAll();
if ("id" in input) {
  query = query.where("id", "=", input.id);
} else {
  query = query.where("sku", "=", (input as { sku: string }).sku);
}
const item = await query.forUpdate().executeTakeFirst();
```

### Custom field extraction

Destructure known fields and treat the rest as custom fields:

```typescript
const { name, barcode, unitId, ...rest } = input as any;
const { id: _id, sku: _sku, ...customFields } = rest;
```

### Why not throw immutable errors

Natural keys like `sku` or `code` are **lookup keys**, not updatable fields. They do not belong in the mutable section of the input type — the type system prevents callers from passing them as update targets. There is no need for a `SkuImmutableError` or `CodeImmutableError` in an update command.

> Exception: conditionally-mutable fields (e.g., `baseCurrencyId` that can only change in DRAFT status) belong in the mutable section and use a business-rule error when the condition is not met.

### Nullable field handling

Match each field's input type to the DB column's nullability. Branching is the same in both cases.

```typescript
export type UpdateXInput = { id: string } & {
  name?: string;               // non-nullable column → no `| null`
  description?: string | null; // nullable column → `| null` enables NULL clear
};

// implementation — same shape regardless of nullability
const updates: Updateable<"X"> = {};
if (name !== undefined) updates.name = name;
if (description !== undefined) updates.description = description;
// undefined: skip / null: NULL clear (nullable only) / value: update
```

**Branching rules:**
- Always use `!== undefined` for "should this column be touched?" decisions in update commands.
- Do **not** use `!= null` for update branching — it conflates "not provided" with "cleared" and prevents NULL clears.

## Document Commands (header + lines)

Transactional documents (purchase order, purchase bill, requisition, sales order, ...) carry a header plus line items. Master-data update commands keep the shape described above; **document** commands use the shapes below.

### Command shapes

| Command | Input |
|---|---|
| create | `{ header, lines }` |
| update | `{ id, headerPatch?, addLines?, updateLines?, removeLineIds? }` |

Naming is fixed: `headerPatch` (omitted = untouched, `null` = clear, value = update), plus three line collections — `addLines` (full new lines), `updateLines` (`{ lineId, linePatch }`), `removeLineIds` (bare ids).

### Line collections

```typescript
// Hand-written per command: patch fields all optional; `| null` only where clearing is allowed
export interface XLinePatch {
  quantity?: string;
  receivingSiteId?: string | null;
}
export interface XLineEdit {
  lineId: string;
  linePatch: XLinePatch;
}
export type UpdateXInput = {
  id: string;
  headerPatch?: XHeaderPatch;
  addLines?: XLineInput[]; // the create line input — a new line is one concept
  updateLines?: XLineEdit[];
  removeLineIds?: string[];
};
```

- Apply `updateLines` as an in-place patch, never as delete + reinsert — the row keeps its `lineId` and `createdAt`.
- Validate the merged row (current + patch) with explicit `!== undefined` — `??` swallows an invalid `null`.

### Output

The command (`run`) returns the header row only (`returningAll()`, so no field list drifts) — not the lines. Callers use it directly; lines are read separately, so don't fetch and embed them here.

```typescript
return ok({ purchaseOrder: updatedHeader });
```

## Conventions

- Input types for create commands: exported interfaces (`export interface CreateXInput`)
- Input types for update commands: exported type aliases (`export type UpdateXInput = ...`)
- Use `.executeTakeFirst()` for single results
- Include JSDoc: `/** Function: name \n Description */`

## State Transitions

The lifecycle object is auto-generated from the model doc's State Transitions table into `db/<model>.lifecycle.generated.ts`.

### Simple transitions (no side effects)

Use `executeTransition` when the command only changes the status field and nothing else:

```typescript
import { ok } from "@tailor-platform/erp-kit/core";
import { executeTransition } from "@tailor-platform/erp-kit/core";
import { orderLifecycle } from "../db/order.lifecycle.generated";
import { OrderNotFoundError, InvalidStateTransitionError } from "../lib/errors.generated";

export interface SubmitOrderInput {
  id: string;
}

export async function run(db: Transaction, input: SubmitOrderInput) {
  const result = await executeTransition({
    db,
    tableName: "Order",
    statusField: "status",
    id: input.id,
    transition: "submit",
    lifecycle: orderLifecycle,
    errors: { notFound: OrderNotFoundError, invalidTransition: InvalidStateTransitionError },
  });
  if (!result.ok) return result;
  return ok({ order: result.value });
}
```

- `transition` must match a key in the lifecycle's transitions map (type-checked)
- `executeTransition` handles forUpdate locking, state validation, and status update
- Error classes must accept `(id: string)` constructor — use generated error classes from `errors.generated.ts`

### Complex transitions (with side effects)

When a transition needs additional logic — creating related records, recalculating fields, calling cross-module commands — use `lifecycle.tryTransition` directly and handle the update yourself. `tryTransition` returns the target state (or `undefined` if the transition is not valid from the current state), so you can use the returned value as the next status instead of hardcoding it:

```typescript
import { ok, err } from "@tailor-platform/erp-kit/core";
import { orderLifecycle } from "../db/order.lifecycle.generated";
import { OrderNotFoundError, InvalidStateTransitionError } from "../lib/errors.generated";

export async function run(db: Transaction, input: ApproveOrderInput) {
  const order = await db
    .selectFrom("Order")
    .selectAll()
    .where("id", "=", input.id)
    .forUpdate()
    .executeTakeFirst();

  if (!order) return err(new OrderNotFoundError(input.id));
  const nextStatus = orderLifecycle.tryTransition(order.status, "approve");
  if (!nextStatus) {
    return err(new InvalidStateTransitionError(input.id));
  }

  // Side effects before status update
  await inventoryCommands.reserveStock(db, { orderId: order.id }, ctx);

  const updated = await db
    .updateTable("Order")
    .set({ status: nextStatus, approvedAt: new Date() })
    .where("id", "=", input.id)
    .returningAll()
    .executeTakeFirstOrThrow();

  return ok({ order: updated });
}
```
