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>
79 lines
2.4 KiB
TypeScript
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
|
|
]);
|
|
}
|
|
}
|