Searching "JOHN" or "ord-1001" returned no orders because SQLite instr() is case-sensitive. Compare lowercased values for order number, customer name, and email. Adds API regression tests for both cases. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HUfxyr6EcRi4EYSX1wLoRh
211 lines
6.6 KiB
TypeScript
211 lines
6.6 KiB
TypeScript
import type { Address, AuditEvent, OrderDetail, OrderItemDetail, OrderSummary } from "@orderops/shared";
|
|
import { and, asc, desc, eq, inArray, or, sql } from "drizzle-orm";
|
|
import type { DatabaseContext } from "../db/database";
|
|
import { addresses, auditEvents, inventory, orderItems, orders, products, users } from "../db/schema";
|
|
|
|
function toAddress(row: typeof addresses.$inferSelect): Address {
|
|
return {
|
|
fullName: row.fullName,
|
|
line1: row.line1,
|
|
line2: row.line2,
|
|
city: row.city,
|
|
state: row.state,
|
|
postalCode: row.postalCode,
|
|
country: row.country
|
|
};
|
|
}
|
|
|
|
function toOrderItemDetail(row: {
|
|
item: typeof orderItems.$inferSelect;
|
|
product: typeof products.$inferSelect;
|
|
inventory: typeof inventory.$inferSelect | null;
|
|
}): OrderItemDetail {
|
|
return {
|
|
id: row.item.id,
|
|
productId: row.product.id,
|
|
sku: row.product.sku,
|
|
productName: row.product.name,
|
|
quantity: row.item.quantity,
|
|
unitPrice: {
|
|
amountCents: row.item.unitPriceCents,
|
|
currency: "USD"
|
|
},
|
|
availableQuantity: row.inventory?.availableQuantity ?? 0
|
|
};
|
|
}
|
|
|
|
export async function listOrderSummaries(context: DatabaseContext, query?: string) {
|
|
const search = query?.trim();
|
|
|
|
// Support staff type search terms in any case. instr() is case-sensitive, so compare lowercased
|
|
// values; LIKE is avoided because it would treat % and _ in the search text as wildcards.
|
|
const filters = search
|
|
? or(
|
|
sql`instr(lower(${orders.orderNumber}), lower(${search})) > 0`,
|
|
sql`instr(lower(${users.name}), lower(${search})) > 0`,
|
|
sql`instr(lower(${users.email}), lower(${search})) > 0`
|
|
)
|
|
: undefined;
|
|
|
|
const rows = await context.db
|
|
.select({
|
|
order: orders,
|
|
customer: users,
|
|
itemCount: sql<number>`count(${orderItems.id})`
|
|
})
|
|
.from(orders)
|
|
.innerJoin(users, eq(orders.customerId, users.id))
|
|
.leftJoin(orderItems, eq(orderItems.orderId, orders.id))
|
|
.where(filters)
|
|
.groupBy(orders.id, users.id)
|
|
.orderBy(desc(orders.createdAt));
|
|
|
|
return rows.map(({ order, customer, itemCount }) => ({
|
|
id: order.id,
|
|
orderNumber: order.orderNumber,
|
|
status: order.status as OrderSummary["status"],
|
|
customer: {
|
|
id: customer.id,
|
|
name: customer.name,
|
|
email: customer.email
|
|
},
|
|
itemCount,
|
|
createdAt: order.createdAt
|
|
})) satisfies OrderSummary[];
|
|
}
|
|
|
|
export async function findOrderDetail(context: DatabaseContext, orderId: string) {
|
|
const [header] = await context.db
|
|
.select({ order: orders, customer: users, address: addresses })
|
|
.from(orders)
|
|
.innerJoin(users, eq(orders.customerId, users.id))
|
|
.innerJoin(addresses, eq(orders.shippingAddressId, addresses.id))
|
|
.where(eq(orders.id, orderId));
|
|
|
|
if (!header) {
|
|
return null;
|
|
}
|
|
|
|
const itemRows = await context.db
|
|
.select({ item: orderItems, product: products, inventory })
|
|
.from(orderItems)
|
|
.innerJoin(products, eq(orderItems.productId, products.id))
|
|
.leftJoin(inventory, eq(orderItems.productId, inventory.productId))
|
|
.where(eq(orderItems.orderId, orderId))
|
|
.orderBy(asc(orderItems.id));
|
|
|
|
return {
|
|
id: header.order.id,
|
|
orderNumber: header.order.orderNumber,
|
|
status: header.order.status as OrderDetail["status"],
|
|
createdAt: header.order.createdAt,
|
|
updatedAt: header.order.updatedAt,
|
|
customer: {
|
|
id: header.customer.id,
|
|
name: header.customer.name,
|
|
email: header.customer.email
|
|
},
|
|
shippingAddress: toAddress(header.address),
|
|
items: itemRows.map(toOrderItemDetail)
|
|
} satisfies OrderDetail;
|
|
}
|
|
|
|
export async function findOrderItemRecord(context: DatabaseContext, orderId: string, itemId: string) {
|
|
const [row] = await context.db
|
|
.select({ order: orders, item: orderItems, inventory })
|
|
.from(orderItems)
|
|
.innerJoin(orders, eq(orderItems.orderId, orders.id))
|
|
.leftJoin(inventory, eq(orderItems.productId, inventory.productId))
|
|
.where(and(eq(orderItems.id, itemId), eq(orderItems.orderId, orderId)));
|
|
|
|
return row ?? null;
|
|
}
|
|
|
|
export async function updateOrderItemQuantity(context: DatabaseContext, itemId: string, quantity: number) {
|
|
await context.db.update(orderItems).set({ quantity }).where(eq(orderItems.id, itemId));
|
|
}
|
|
|
|
export async function updateOrderTimestamp(context: DatabaseContext, orderId: string, updatedAt: string) {
|
|
await context.db.update(orders).set({ updatedAt }).where(eq(orders.id, orderId));
|
|
}
|
|
|
|
export async function updateOrderStatus(context: DatabaseContext, orderId: string, status: OrderDetail["status"], updatedAt: string) {
|
|
await context.db.update(orders).set({ status, updatedAt }).where(eq(orders.id, orderId));
|
|
}
|
|
|
|
export async function updateOrderAddress(context: DatabaseContext, orderId: string, address: Address) {
|
|
const [row] = await context.db.select({ shippingAddressId: orders.shippingAddressId }).from(orders).where(eq(orders.id, orderId));
|
|
|
|
if (!row) {
|
|
return false;
|
|
}
|
|
|
|
await context.db
|
|
.update(addresses)
|
|
.set({
|
|
fullName: address.fullName,
|
|
line1: address.line1,
|
|
line2: address.line2,
|
|
city: address.city,
|
|
state: address.state,
|
|
postalCode: address.postalCode,
|
|
country: address.country
|
|
})
|
|
.where(eq(addresses.id, row.shippingAddressId));
|
|
|
|
return true;
|
|
}
|
|
|
|
export async function appendAuditEvent(
|
|
context: DatabaseContext,
|
|
event: {
|
|
orderId: string;
|
|
actor: string;
|
|
type: string;
|
|
message: string;
|
|
createdAt: string;
|
|
}
|
|
) {
|
|
await context.db.insert(auditEvents).values({
|
|
id: crypto.randomUUID(),
|
|
orderId: event.orderId,
|
|
actor: event.actor,
|
|
type: event.type,
|
|
message: event.message,
|
|
createdAt: event.createdAt
|
|
});
|
|
}
|
|
|
|
async function findCustomerSummaryByOrderId(context: DatabaseContext, orderId: string) {
|
|
const [row] = await context.db
|
|
.select({ customer: users })
|
|
.from(orders)
|
|
.innerJoin(users, eq(orders.customerId, users.id))
|
|
.where(eq(orders.id, orderId));
|
|
|
|
return row?.customer ?? null;
|
|
}
|
|
|
|
export async function listOrderAuditEvents(context: DatabaseContext, orderId: string) {
|
|
const events = await context.db.select().from(auditEvents).where(eq(auditEvents.orderId, orderId)).orderBy(desc(auditEvents.createdAt));
|
|
|
|
return Promise.all(
|
|
events.map(async (event) => {
|
|
await findCustomerSummaryByOrderId(context, event.orderId);
|
|
|
|
return {
|
|
id: event.id,
|
|
orderId: event.orderId,
|
|
type: event.type,
|
|
message: event.message,
|
|
createdAt: event.createdAt,
|
|
actor: event.actor
|
|
} satisfies AuditEvent;
|
|
})
|
|
);
|
|
}
|
|
|
|
export async function listOrdersByIds(context: DatabaseContext, orderIds: string[]) {
|
|
return context.db.select().from(orders).where(inArray(orders.id, orderIds));
|
|
}
|