import type { RowDataPacket } from 'mysql2/promise'; import type { AccessProfile } from '../auth/rbac-repository.js'; import type { MySqlPool } from '../db/mysql.js'; import type { OrderStatus } from './order-state-repository.js'; export class OrderQueryError extends Error { constructor(public readonly code: string) { super(code); } } export interface OrderQueryAccess { tenantId: string; userId: string; access: AccessProfile; } interface OrderListRow extends RowDataPacket { id: string; orderNo: string; storeId: string; storeName: string; roomId: string; roomName: string; roomNo: string; status: OrderStatus; startAt: Date; endAt: Date; totalAmountCents: number; paidAmountCents: number; latestPaymentId: string | null; latestPaymentProvider: string | null; latestPaymentStatus: string | null; createdAt: Date; } interface CountRow extends RowDataPacket { total: number } export class OrderQueryRepository { constructor(private readonly pool: MySqlPool) {} async listMine(input: OrderQueryAccess & { page: number; pageSize: number; status?: OrderStatus; }) { const where = [ 'o.tenant_id = ?', 'o.deleted_at IS NULL', '(a.user_id = ? OR ' + managerScopeSql(input.access, 'o.store_id') + ')' ]; const params: Array = [input.tenantId, input.userId]; if (input.status) { where.push('o.status = ?'); params.push(input.status); } const whereSql = where.join(' AND '); const offset = (input.page - 1) * input.pageSize; const [counts] = await this.pool.execute( `SELECT COUNT(DISTINCT o.id) AS total FROM qipai_orders o LEFT JOIN qipai_order_user_access a ON a.tenant_id = o.tenant_id AND a.order_id = o.id AND a.revoked_at IS NULL WHERE ${whereSql}`, params ); const [rows] = await this.pool.execute( `SELECT DISTINCT o.id, o.order_no AS orderNo, o.store_id AS storeId, s.name AS storeName, o.room_id AS roomId, r.name AS roomName, r.room_no AS roomNo, o.status, o.start_at AS startAt, o.end_at AS endAt, o.total_amount_cents AS totalAmountCents, o.paid_amount_cents AS paidAmountCents, p.id AS latestPaymentId, p.provider AS latestPaymentProvider, p.status AS latestPaymentStatus, o.created_at AS createdAt FROM qipai_orders o INNER JOIN qipai_stores s ON s.tenant_id = o.tenant_id AND s.id = o.store_id INNER JOIN qipai_rooms r ON r.tenant_id = o.tenant_id AND r.id = o.room_id LEFT JOIN qipai_order_user_access a ON a.tenant_id = o.tenant_id AND a.order_id = o.id AND a.revoked_at IS NULL LEFT JOIN qipai_payments p ON p.tenant_id = o.tenant_id AND p.order_id = o.id AND p.id = ( SELECT MAX(p2.id) FROM qipai_payments p2 WHERE p2.tenant_id = o.tenant_id AND p2.order_id = o.id AND p2.deleted_at IS NULL ) WHERE ${whereSql} ORDER BY o.created_at DESC, o.id DESC LIMIT ? OFFSET ?`, [...params, input.pageSize, offset] ); return { items: rows.map(publicOrder), total: Number(counts[0]?.total ?? 0), page: input.page, pageSize: input.pageSize }; } async getMine(input: OrderQueryAccess & { orderId: string }) { const result = await this.listMine({ ...input, page: 1, pageSize: 1 }); const order = result.items.find((item) => item.id === input.orderId); if (order) return order; const [rows] = await this.pool.execute( `SELECT o.id, o.order_no AS orderNo, o.store_id AS storeId, s.name AS storeName, o.room_id AS roomId, r.name AS roomName, r.room_no AS roomNo, o.status, o.start_at AS startAt, o.end_at AS endAt, o.total_amount_cents AS totalAmountCents, o.paid_amount_cents AS paidAmountCents, p.id AS latestPaymentId, p.provider AS latestPaymentProvider, p.status AS latestPaymentStatus, o.created_at AS createdAt FROM qipai_orders o INNER JOIN qipai_stores s ON s.tenant_id = o.tenant_id AND s.id = o.store_id INNER JOIN qipai_rooms r ON r.tenant_id = o.tenant_id AND r.id = o.room_id LEFT JOIN qipai_order_user_access a ON a.tenant_id = o.tenant_id AND a.order_id = o.id AND a.revoked_at IS NULL LEFT JOIN qipai_payments p ON p.tenant_id = o.tenant_id AND p.order_id = o.id AND p.id = ( SELECT MAX(p2.id) FROM qipai_payments p2 WHERE p2.tenant_id = o.tenant_id AND p2.order_id = o.id AND p2.deleted_at IS NULL ) WHERE o.tenant_id = ? AND o.id = ? AND o.deleted_at IS NULL AND (a.user_id = ? OR ${managerScopeSql(input.access, 'o.store_id')}) LIMIT 1`, [input.tenantId, input.orderId, input.userId] ); if (!rows[0]) throw new OrderQueryError('ORDER_NOT_FOUND'); return publicOrder(rows[0]); } } function publicOrder(row: OrderListRow) { return { id: String(row.id), orderNo: row.orderNo, storeId: String(row.storeId), storeName: row.storeName, roomId: String(row.roomId), roomName: row.roomName, roomNo: row.roomNo, status: row.status, startAt: row.startAt, endAt: row.endAt, totalAmountCents: row.totalAmountCents, paidAmountCents: row.paidAmountCents, latestPayment: row.latestPaymentId === null ? null : { id: String(row.latestPaymentId), provider: row.latestPaymentProvider, status: row.latestPaymentStatus }, createdAt: row.createdAt }; } function managerScopeSql(access: AccessProfile, storeExpression: string) { if (access.capabilities.includes('tenant.manage') || access.roles.includes('PLATFORM_ADMIN')) { return '1 = 1'; } if (!access.capabilities.includes('store.operation.read') || access.storeIds.length === 0) { return '1 = 0'; } return `${storeExpression} IN (${access.storeIds.map((id) => Number(id)).join(',')})`; }