Without a persistent order history log, you cannot answer the questions that matter most during a dispute or refund: When did the order ship? Who changed the status? Was the cancellation before or after fulfillment? The only surviving evidence is the current status field and a single updatedAt timestamp that gets overwritten on every subsequent change. CWE-778 (Insufficient Logging) and GDPR Art. 5(1)(f) both require that processing activities are traceable and reconstructable. An updatedAt column proves nothing — it's a cursor, not a log.
High because missing history makes it impossible to reconstruct the order timeline during disputes, refund requests, or compliance audits, and the gap can never be backfilled after the fact.
Create an order_history table and write to it atomically inside the same database transaction as every status update. Use db.$transaction to guarantee both writes succeed or neither does.
// prisma/schema.prisma addition:
// model OrderHistory {
// id String @id @default(cuid())
// orderId String
// fromStatus String?
// toStatus String
// actorId String?
// note String?
// createdAt DateTime @default(now())
// order Order @relation(fields: [orderId], references: [id])
// }
// lib/orders/state-machine.ts
await db.$transaction([
db.orders.update({ where: { id: orderId }, data: { status: newStatus } }),
db.orderHistory.create({
data: { orderId, fromStatus: order.status, toStatus: newStatus, actorId, note },
}),
])
ID: ecommerce-order-management.order-lifecycle.history-log
Severity: high
What to look for: List all status-transition code paths identified in the state-transitions check. For each path, determine whether it also writes a history entry. Look for an order history or audit log mechanism: a separate order_history or order_events database table, an embedded history array field on the order document, or a generic audit log table. Count how many of the status-transition paths write history entries. Check whether history entries capture at least 3 of these fields: the previous status, the new status, a timestamp, and the actor. A history mechanism that covers fewer than 100% of transition paths must not pass.
Pass criteria: Every order status change (100% of transition paths) creates a persistent history entry containing at minimum the new status and a timestamp. History entries are stored in a durable way (database table or document array) that survives application restarts. The history logging appears alongside all status transition code paths, not just a subset. Report the ratio even on pass: "N of N transition paths write history entries."
Fail criteria: No order history tracking mechanism exists — only the current order.status field is updated with no record of what it was before. Or history logging exists but covers fewer than 100% of transition paths — called in the checkout confirmation handler but not in the admin status update handler, for example. An updatedAt timestamp alone does not count as pass — it gets overwritten on subsequent changes.
Skip (N/A) when: The project uses an external order management platform (Shopify Admin API, Medusa) that natively handles all history logging with its own audit trail, and 0 status writes occur in application code.
Detail on fail: Describe what exists and what is missing. Example: "Order status updates are applied directly to the orders table with no history logging. No order_history table or equivalent exists in the schema. 0 of 4 transition paths write history entries."
Remediation: Create an order_history table (or OrderHistory model in prisma/schema.prisma) and write to it atomically with every status transition:
// In your Prisma schema (prisma/schema.prisma):
// model OrderHistory {
// id String @id @default(cuid())
// orderId String
// fromStatus String?
// toStatus String
// note String?
// actorId String?
// createdAt DateTime @default(now())
// order Order @relation(fields: [orderId], references: [id])
// }
// lib/orders/state-machine.ts
export async function transitionOrder(
orderId: string,
newStatus: string,
actorId?: string,
note?: string
) {
const order = await db.orders.findUniqueOrThrow({ where: { id: orderId } })
// ... validate transition ...
await db.$transaction([
db.orders.update({
where: { id: orderId },
data: { status: newStatus, updatedAt: new Date() },
}),
db.orderHistory.create({
data: {
orderId,
fromStatus: order.status,
toStatus: newStatus,
actorId,
note,
},
}),
])
}
Using a database transaction ensures the status update and history entry are always written together, preventing partial writes.