Files
redefined-designs/backend/src/intake/applyDraft.ts
T
bermudalambandClaude Opus 5 511022d248 feat(intake): record a draft without publishing it (#223)
The copy goes on item_drafts, never on the item. The item keeps its placeholder name and description until a person approves them in the review queue (#225) — nothing a model wrote reaches the catalogue unreviewed.

The price is the deliberate exception, because #220 chose to price an item on arrival rather than leave it unpriced. price_source records that the number came from a model rather than a person, so the review queue can show it as unconfirmed. With no suggestion the item keeps the migration's 8000 default and price_source stays 'default'; the queue shows both the same way, as a number nobody has chosen yet.

A category is checked against the real table before it is stored. The schema constrains the shape of the answer but cannot enforce membership, and a category the shop does not have would be invisible to every storefront filter — a draft nobody could find, rather than an obvious error.

A successful retry clears ai_error, or a draft that eventually worked would still read as broken in the queue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 08:32:40 -05:00

79 lines
2.4 KiB
TypeScript

import { PoolClient } from 'pg';
import { DraftOutcome } from './draftListing';
/**
* Writes a finished draft to the database.
*
* The copy goes on `item_drafts`, never on the item. The item keeps its
* placeholder name and description until a person approves them in the review
* queue (#225) — nothing a model wrote reaches the catalogue unreviewed.
*
* The price is the deliberate exception, because #220 chose to price an item on
* arrival rather than leave it unpriced. `price_source` records that the number
* came from a model rather than a person, which is what lets the review queue
* show it as unconfirmed.
*/
export async function applyDraft(
client: PoolClient,
itemId: number,
outcome: DraftOutcome
): Promise<void> {
const { draft } = outcome;
// Checked against the real table before it is stored. The schema constrains
// the shape of the answer but cannot enforce membership, and a category the
// shop does not have would be invisible to every storefront filter — a draft
// nobody could find, rather than an obvious error.
const categoryId = draft.category === null
? null
: (
await client.query<{ id: number }>(
`SELECT id FROM categories WHERE lower(name) = lower($1)`,
[draft.category]
)
).rows[0]?.id ?? null;
const hasPrice = draft.suggestedPriceCents !== null;
await client.query(
`UPDATE item_drafts
SET state = 'ready',
model = $2,
ai_name = $3,
ai_description = $4,
ai_category_id = $5,
ai_tag_names = $6,
ai_suggested_price_cents = $7,
price_source = $8,
input_tokens = $9,
output_tokens = $10,
cost_micros = $11,
ai_error = NULL,
drafted_at = now()
WHERE item_id = $1`,
[
itemId,
outcome.model,
draft.name,
draft.description,
categoryId,
draft.tags,
draft.suggestedPriceCents,
hasPrice ? 'ai' : 'default',
outcome.inputTokens,
outcome.outputTokens,
outcome.costMicros
]
);
// Only when there is one. Absent, the item keeps the migration's default and
// price_source stays 'default' — the review queue shows both the same way, as
// a number nobody has chosen yet.
if (hasPrice) {
await client.query(`UPDATE items SET price_cents = $2 WHERE id = $1`, [
itemId,
draft.suggestedPriceCents
]);
}
}