feat(M04-B): 完成订单状态机与迁移历史
This commit is contained in:
@@ -30,6 +30,9 @@ import {
|
||||
type StoreAccessRouteOptions
|
||||
} from './routes/store-access.js';
|
||||
import { registerPricingRoutes, type PricingRouteOptions } from './routes/pricing.js';
|
||||
import {
|
||||
registerOrderStateRoutes, type OrderStateRouteOptions
|
||||
} from './routes/order-state.js';
|
||||
|
||||
export interface BuildAppOptions {
|
||||
config?: AppConfig;
|
||||
@@ -41,6 +44,7 @@ export interface BuildAppOptions {
|
||||
storeDiscovery?: StoreDiscoveryRouteOptions;
|
||||
storeAccess?: StoreAccessRouteOptions;
|
||||
pricing?: PricingRouteOptions;
|
||||
orderState?: OrderStateRouteOptions;
|
||||
}
|
||||
|
||||
declare module 'fastify' {
|
||||
@@ -107,6 +111,9 @@ export async function buildApp(options: BuildAppOptions = {}): Promise<FastifyIn
|
||||
if (options.pricing) {
|
||||
await registerPricingRoutes(app, options.pricing);
|
||||
}
|
||||
if (options.orderState) {
|
||||
await registerOrderStateRoutes(app, options.orderState);
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ const migrationFiles: Record<MigrationDirection, readonly string[]> = {
|
||||
'database/migrations/2026061808_m03b_decoration_ads_media.up.sql',
|
||||
'database/migrations/2026061809_m03c_store_discovery.up.sql',
|
||||
'database/migrations/2026061810_m03d_scene_wifi_access.up.sql',
|
||||
'database/migrations/2026061811_m04a_pricing_reservations.up.sql'
|
||||
'database/migrations/2026061811_m04a_pricing_reservations.up.sql',
|
||||
'database/migrations/2026062012_m04b_order_state_machine.up.sql'
|
||||
],
|
||||
verify: [
|
||||
'database/migrations/2026061601_m01b_core_schema.verify.sql',
|
||||
@@ -44,9 +45,11 @@ const migrationFiles: Record<MigrationDirection, readonly string[]> = {
|
||||
'database/migrations/2026061808_m03b_decoration_ads_media.verify.sql',
|
||||
'database/migrations/2026061809_m03c_store_discovery.verify.sql',
|
||||
'database/migrations/2026061810_m03d_scene_wifi_access.verify.sql',
|
||||
'database/migrations/2026061811_m04a_pricing_reservations.verify.sql'
|
||||
'database/migrations/2026061811_m04a_pricing_reservations.verify.sql',
|
||||
'database/migrations/2026062012_m04b_order_state_machine.verify.sql'
|
||||
],
|
||||
down: [
|
||||
'database/migrations/2026062012_m04b_order_state_machine.down.sql',
|
||||
'database/migrations/2026061811_m04a_pricing_reservations.down.sql',
|
||||
'database/migrations/2026061810_m03d_scene_wifi_access.down.sql',
|
||||
'database/migrations/2026061809_m03c_store_discovery.down.sql',
|
||||
@@ -184,7 +187,8 @@ export async function executeMigrationPlan(
|
||||
3, 3, 1,
|
||||
2, 2, 1,
|
||||
3, 3, 1,
|
||||
2, 1, 3, 3, 1
|
||||
2, 1, 3, 3, 1,
|
||||
2, 1, 3, 1
|
||||
][index] ?? 1;
|
||||
if (!Array.isArray(result) || result.length < minimumRows) {
|
||||
throw new Error(
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import type { PoolConnection, ResultSetHeader, RowDataPacket } from 'mysql2/promise';
|
||||
import type { AccessProfile } from '../auth/rbac-repository.js';
|
||||
import type { MySqlPool } from '../db/mysql.js';
|
||||
|
||||
export const orderActions = [
|
||||
'SUBMIT', 'CONFIRM_PAYMENT', 'RESERVE', 'START', 'FINISH', 'CANCEL',
|
||||
'BEGIN_REFUND', 'COMPLETE_REFUND', 'CLOSE'
|
||||
] as const;
|
||||
export type OrderAction = typeof orderActions[number];
|
||||
export type OrderStatus =
|
||||
| 'DRAFT' | 'PENDING_PAYMENT' | 'PAID' | 'RESERVED' | 'IN_PROGRESS'
|
||||
| 'FINISHED' | 'CANCELLED' | 'REFUNDING' | 'REFUNDED' | 'CLOSED';
|
||||
|
||||
export interface OrderActor {
|
||||
tenantId: string;
|
||||
userId: string;
|
||||
actorType: 'USER' | 'SYSTEM';
|
||||
source: 'APP' | 'ADMIN' | 'PAYMENT' | 'WORKER' | 'SYSTEM';
|
||||
traceId: string;
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
access?: AccessProfile;
|
||||
}
|
||||
|
||||
interface OrderRow extends RowDataPacket {
|
||||
id: string;
|
||||
storeId: string;
|
||||
status: OrderStatus;
|
||||
statusVersion: number;
|
||||
}
|
||||
interface HistoryRow extends RowDataPacket {
|
||||
id: string;
|
||||
fromStatus: OrderStatus | null;
|
||||
toStatus: OrderStatus;
|
||||
action: OrderAction | 'CREATED' | 'EXPIRED' | 'MIGRATED';
|
||||
actorType: string;
|
||||
actorId: string | null;
|
||||
source: string;
|
||||
reason: string;
|
||||
traceId: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
const targetByAction: Record<OrderAction, OrderStatus> = {
|
||||
SUBMIT: 'PENDING_PAYMENT',
|
||||
CONFIRM_PAYMENT: 'PAID',
|
||||
RESERVE: 'RESERVED',
|
||||
START: 'IN_PROGRESS',
|
||||
FINISH: 'FINISHED',
|
||||
CANCEL: 'CANCELLED',
|
||||
BEGIN_REFUND: 'REFUNDING',
|
||||
COMPLETE_REFUND: 'REFUNDED',
|
||||
CLOSE: 'CLOSED'
|
||||
};
|
||||
|
||||
const allowedActions: Record<OrderStatus, readonly OrderAction[]> = {
|
||||
DRAFT: ['SUBMIT', 'CANCEL', 'CLOSE'],
|
||||
PENDING_PAYMENT: ['CONFIRM_PAYMENT', 'CANCEL', 'CLOSE'],
|
||||
PAID: ['RESERVE', 'START', 'CANCEL', 'BEGIN_REFUND'],
|
||||
RESERVED: ['START', 'CANCEL', 'BEGIN_REFUND'],
|
||||
IN_PROGRESS: ['FINISH', 'BEGIN_REFUND'],
|
||||
FINISHED: ['BEGIN_REFUND', 'CLOSE'],
|
||||
CANCELLED: ['BEGIN_REFUND', 'CLOSE'],
|
||||
REFUNDING: ['COMPLETE_REFUND'],
|
||||
REFUNDED: ['CLOSE'],
|
||||
CLOSED: []
|
||||
};
|
||||
|
||||
export class OrderStateError extends Error {
|
||||
constructor(public readonly code: string) { super(code); }
|
||||
}
|
||||
|
||||
export class OrderStateRepository {
|
||||
constructor(private readonly pool: MySqlPool) {}
|
||||
|
||||
async transition(actor: OrderActor, orderId: string, action: OrderAction, reason = '') {
|
||||
return this.transaction(async (connection) => {
|
||||
const order = await this.loadOrder(connection, actor.tenantId, orderId, true);
|
||||
await this.assertAuthorized(connection, actor, order, action);
|
||||
const duplicate = await this.findByTrace(connection, actor.tenantId, orderId, actor.traceId);
|
||||
if (duplicate) {
|
||||
return {
|
||||
orderId,
|
||||
status: duplicate.toStatus,
|
||||
statusVersion: null,
|
||||
historyId: duplicate.id,
|
||||
idempotent: true
|
||||
};
|
||||
}
|
||||
if (!allowedActions[order.status].includes(action)) {
|
||||
throw new OrderStateError('ORDER_TRANSITION_NOT_ALLOWED');
|
||||
}
|
||||
const targetStatus = targetByAction[action];
|
||||
const nextVersion = Number(order.statusVersion) + 1;
|
||||
await connection.execute(
|
||||
`UPDATE qipai_orders
|
||||
SET status = ?, status_version = ?, status_updated_at = UTC_TIMESTAMP(3)
|
||||
WHERE tenant_id = ? AND id = ?`,
|
||||
[targetStatus, nextVersion, actor.tenantId, orderId]
|
||||
);
|
||||
await this.applyReservationState(connection, actor.tenantId, orderId, targetStatus);
|
||||
const [result] = await connection.execute<ResultSetHeader>(
|
||||
`INSERT INTO qipai_order_status_history
|
||||
(tenant_id, order_id, from_status, to_status, action, actor_type,
|
||||
actor_id, source, reason, trace_id, metadata)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, JSON_OBJECT('statusVersion', ?))`,
|
||||
[actor.tenantId, orderId, order.status, targetStatus, action,
|
||||
actor.actorType, actor.userId, actor.source, reason.slice(0, 512),
|
||||
actor.traceId, nextVersion]
|
||||
);
|
||||
await connection.execute(
|
||||
`INSERT INTO qipai_audit_logs
|
||||
(tenant_id, actor_type, actor_id, action, resource_type, resource_id,
|
||||
trace_id, ip, user_agent, metadata)
|
||||
VALUES (?, ?, ?, 'ORDER_STATUS_CHANGED', 'ORDER', ?, ?, ?, ?,
|
||||
JSON_OBJECT('fromStatus', ?, 'toStatus', ?, 'orderAction', ?))`,
|
||||
[actor.tenantId, actor.actorType, actor.userId, orderId, actor.traceId,
|
||||
actor.ip, actor.userAgent.slice(0, 255), order.status, targetStatus, action]
|
||||
);
|
||||
return {
|
||||
orderId,
|
||||
status: targetStatus,
|
||||
statusVersion: nextVersion,
|
||||
historyId: String(result.insertId),
|
||||
idempotent: false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async history(tenantId: string, userId: string, orderId: string, access: AccessProfile) {
|
||||
const order = await this.loadOrder(this.pool, tenantId, orderId, false);
|
||||
const manager = canManageStore(access, order.storeId);
|
||||
if (!manager) {
|
||||
const [rows] = await this.pool.execute<RowDataPacket[]>(
|
||||
`SELECT 1 FROM qipai_order_user_access
|
||||
WHERE tenant_id = ? AND order_id = ? AND user_id = ?`,
|
||||
[tenantId, orderId, userId]
|
||||
);
|
||||
if (!rows[0]) throw new OrderStateError('ORDER_ACCESS_FORBIDDEN');
|
||||
}
|
||||
const [rows] = await this.pool.execute<HistoryRow[]>(
|
||||
`SELECT id, from_status AS fromStatus, to_status AS toStatus, action,
|
||||
actor_type AS actorType, actor_id AS actorId, source, reason,
|
||||
trace_id AS traceId, created_at AS createdAt
|
||||
FROM qipai_order_status_history
|
||||
WHERE tenant_id = ? AND order_id = ? ORDER BY id`,
|
||||
[tenantId, orderId]
|
||||
);
|
||||
return rows.map((row) => ({
|
||||
...row,
|
||||
id: String(row.id),
|
||||
actorId: row.actorId === null ? null : String(row.actorId)
|
||||
}));
|
||||
}
|
||||
|
||||
private async assertAuthorized(
|
||||
connection: PoolConnection, actor: OrderActor, order: OrderRow, action: OrderAction
|
||||
) {
|
||||
if (actor.source !== 'APP') {
|
||||
if (!actor.access || !canManageStore(actor.access, order.storeId)) {
|
||||
throw new OrderStateError('ORDER_MANAGEMENT_FORBIDDEN');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (action !== 'CANCEL') throw new OrderStateError('ORDER_ACTION_FORBIDDEN');
|
||||
const [rows] = await connection.execute<RowDataPacket[]>(
|
||||
`SELECT 1 FROM qipai_order_user_access
|
||||
WHERE tenant_id = ? AND order_id = ? AND user_id = ? AND revoked_at IS NULL`,
|
||||
[actor.tenantId, order.id, actor.userId]
|
||||
);
|
||||
if (!rows[0]) throw new OrderStateError('ORDER_ACCESS_FORBIDDEN');
|
||||
}
|
||||
|
||||
private async loadOrder(
|
||||
connection: Pick<MySqlPool, 'execute'> | PoolConnection,
|
||||
tenantId: string,
|
||||
orderId: string,
|
||||
lock: boolean
|
||||
) {
|
||||
const [rows] = await connection.execute<OrderRow[]>(
|
||||
`SELECT id, store_id AS storeId, status, status_version AS statusVersion
|
||||
FROM qipai_orders
|
||||
WHERE tenant_id = ? AND id = ? AND deleted_at IS NULL
|
||||
${lock ? 'FOR UPDATE' : ''}`,
|
||||
[tenantId, orderId]
|
||||
);
|
||||
if (!rows[0]) throw new OrderStateError('ORDER_NOT_FOUND');
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
private async findByTrace(
|
||||
connection: PoolConnection, tenantId: string, orderId: string, traceId: string
|
||||
) {
|
||||
const [rows] = await connection.execute<HistoryRow[]>(
|
||||
`SELECT id, to_status AS toStatus
|
||||
FROM qipai_order_status_history
|
||||
WHERE tenant_id = ? AND order_id = ? AND trace_id = ? LIMIT 1`,
|
||||
[tenantId, orderId, traceId]
|
||||
);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
private async applyReservationState(
|
||||
connection: PoolConnection, tenantId: string, orderId: string, status: OrderStatus
|
||||
) {
|
||||
if (['PAID', 'RESERVED', 'IN_PROGRESS', 'FINISHED'].includes(status)) {
|
||||
await connection.execute(
|
||||
`UPDATE qipai_room_reservations
|
||||
SET status = 'CONSUMED', expires_at = GREATEST(expires_at, ends_at)
|
||||
WHERE tenant_id = ? AND order_id = ? AND status = 'HELD'`,
|
||||
[tenantId, orderId]
|
||||
);
|
||||
} else if (['CANCELLED', 'REFUNDED', 'CLOSED'].includes(status)) {
|
||||
await connection.execute(
|
||||
`UPDATE qipai_room_reservations
|
||||
SET status = 'RELEASED', released_at = COALESCE(released_at, UTC_TIMESTAMP(3))
|
||||
WHERE tenant_id = ? AND order_id = ? AND status IN ('HELD', 'CONSUMED')`,
|
||||
[tenantId, orderId]
|
||||
);
|
||||
await connection.execute(
|
||||
`UPDATE qipai_order_user_access
|
||||
SET revoked_at = COALESCE(revoked_at, UTC_TIMESTAMP(3))
|
||||
WHERE tenant_id = ? AND order_id = ?`,
|
||||
[tenantId, orderId]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async transaction<T>(work: (connection: PoolConnection) => Promise<T>) {
|
||||
const connection = await this.pool.getConnection();
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
const result = await work(connection);
|
||||
await connection.commit();
|
||||
return result;
|
||||
} catch (error) {
|
||||
await connection.rollback();
|
||||
throw error;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function canManageStore(access: AccessProfile, storeId: string) {
|
||||
return access.capabilities.includes('tenant.manage')
|
||||
|| access.roles.includes('PLATFORM_ADMIN')
|
||||
|| (access.capabilities.includes('store.operation.write') && access.storeIds.includes(storeId));
|
||||
}
|
||||
@@ -97,6 +97,14 @@ export class PricingRepository {
|
||||
VALUES (?, ?, ?)`,
|
||||
[input.tenantId, orderId, input.userId]
|
||||
);
|
||||
await connection.execute(
|
||||
`INSERT INTO qipai_order_status_history
|
||||
(tenant_id, order_id, from_status, to_status, action, actor_type,
|
||||
actor_id, source, reason, trace_id, metadata)
|
||||
VALUES (?, ?, NULL, 'PENDING_PAYMENT', 'CREATED', 'USER', ?,
|
||||
'APP', 'Room hold created', ?, JSON_OBJECT('statusVersion', 1))`,
|
||||
[input.tenantId, orderId, input.userId, `order-created-${orderId}`]
|
||||
);
|
||||
return {
|
||||
orderId,
|
||||
orderNo,
|
||||
@@ -127,12 +135,26 @@ export class PricingRepository {
|
||||
WHERE ${filters.join(' AND ')}`,
|
||||
params
|
||||
);
|
||||
await connection.execute<ResultSetHeader>(
|
||||
`INSERT IGNORE INTO qipai_order_status_history
|
||||
(tenant_id, order_id, from_status, to_status, action, actor_type,
|
||||
actor_id, source, reason, trace_id, metadata)
|
||||
SELECT o.tenant_id, o.id, o.status, 'CLOSED', 'EXPIRED', 'SYSTEM',
|
||||
NULL, 'WORKER', 'Payment hold expired',
|
||||
CONCAT('hold-expired-', o.id), JSON_OBJECT('statusVersion', o.status_version + 1)
|
||||
FROM qipai_room_reservations r
|
||||
INNER JOIN qipai_orders o
|
||||
ON o.id = r.order_id AND o.tenant_id = r.tenant_id
|
||||
WHERE ${filters.join(' AND ')}`,
|
||||
params
|
||||
);
|
||||
await connection.execute<ResultSetHeader>(
|
||||
`UPDATE qipai_room_reservations r
|
||||
INNER JOIN qipai_orders o
|
||||
ON o.id = r.order_id AND o.tenant_id = r.tenant_id
|
||||
SET r.status = 'RELEASED', r.released_at = UTC_TIMESTAMP(3),
|
||||
o.status = 'CLOSED'
|
||||
o.status = 'CLOSED', o.status_version = o.status_version + 1,
|
||||
o.status_updated_at = UTC_TIMESTAMP(3)
|
||||
WHERE ${filters.join(' AND ')}`,
|
||||
params
|
||||
);
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import type { FastifyInstance, FastifyReply } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import type { AuthRepository } from '../auth/auth-repository.js';
|
||||
import { authenticateAccessToken } from '../auth/authenticate.js';
|
||||
import type { AccessProfile } from '../auth/rbac-repository.js';
|
||||
import {
|
||||
OrderStateError, orderActions, type OrderStateRepository
|
||||
} from '../orders/order-state-repository.js';
|
||||
|
||||
const paramsSchema = z.object({ orderId: z.string().regex(/^[1-9]\d{0,19}$/) });
|
||||
const transitionSchema = z.object({
|
||||
action: z.enum(orderActions),
|
||||
reason: z.string().max(512).default('')
|
||||
}).strict();
|
||||
const cancelSchema = z.object({ reason: z.string().max(512).default('') }).strict();
|
||||
|
||||
export interface OrderStateRouteOptions {
|
||||
repository: Pick<OrderStateRepository, 'transition' | 'history'>;
|
||||
authRepository: Pick<AuthRepository, 'validateSession'>;
|
||||
accessControl: { getAccessProfile(tenantId: string, userId: string): Promise<AccessProfile> };
|
||||
jwtSecret: string;
|
||||
}
|
||||
|
||||
export async function registerOrderStateRoutes(
|
||||
app: FastifyInstance, options: OrderStateRouteOptions
|
||||
) {
|
||||
app.get('/app-api/orders/:orderId/history', async (request, reply) => {
|
||||
const auth = await authenticate(request.headers.authorization, options);
|
||||
const params = paramsSchema.safeParse(request.params);
|
||||
if (!auth) return unauthorized(reply, request.traceId);
|
||||
if (!params.success) return invalid(reply, request.traceId);
|
||||
return handle(reply, request.traceId, async () => ({
|
||||
code: 0,
|
||||
data: await options.repository.history(
|
||||
auth.tenantId, auth.userId, params.data.orderId, auth.access
|
||||
),
|
||||
traceId: request.traceId
|
||||
}));
|
||||
});
|
||||
|
||||
app.post('/app-api/orders/:orderId/cancel', async (request, reply) => {
|
||||
const auth = await authenticate(request.headers.authorization, options);
|
||||
const params = paramsSchema.safeParse(request.params);
|
||||
const body = cancelSchema.safeParse(request.body ?? {});
|
||||
if (!auth) return unauthorized(reply, request.traceId);
|
||||
if (!params.success || !body.success) return invalid(reply, request.traceId);
|
||||
return handle(reply, request.traceId, async () => ({
|
||||
code: 0,
|
||||
data: await options.repository.transition({
|
||||
tenantId: auth.tenantId, userId: auth.userId, actorType: 'USER',
|
||||
source: 'APP', traceId: request.traceId, ip: request.ip,
|
||||
userAgent: request.headers['user-agent'] ?? '', access: auth.access
|
||||
}, params.data.orderId, 'CANCEL', body.data.reason),
|
||||
traceId: request.traceId
|
||||
}));
|
||||
});
|
||||
|
||||
app.post('/admin-api/orders/:orderId/actions', async (request, reply) => {
|
||||
const auth = await authenticate(request.headers.authorization, options);
|
||||
const params = paramsSchema.safeParse(request.params);
|
||||
const body = transitionSchema.safeParse(request.body);
|
||||
if (!auth) return unauthorized(reply, request.traceId);
|
||||
if (!params.success || !body.success) return invalid(reply, request.traceId);
|
||||
return handle(reply, request.traceId, async () => ({
|
||||
code: 0,
|
||||
data: await options.repository.transition({
|
||||
tenantId: auth.tenantId, userId: auth.userId, actorType: 'USER',
|
||||
source: 'ADMIN', traceId: request.traceId, ip: request.ip,
|
||||
userAgent: request.headers['user-agent'] ?? '', access: auth.access
|
||||
}, params.data.orderId, body.data.action, body.data.reason),
|
||||
traceId: request.traceId
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
async function authenticate(
|
||||
authorization: string | undefined, options: OrderStateRouteOptions
|
||||
) {
|
||||
const result = await authenticateAccessToken(
|
||||
authorization, options.authRepository, options.jwtSecret
|
||||
);
|
||||
if (!result) return null;
|
||||
const tenantId = result.session.tenantId;
|
||||
const userId = result.session.user.id;
|
||||
return {
|
||||
tenantId,
|
||||
userId,
|
||||
access: await options.accessControl.getAccessProfile(tenantId, userId)
|
||||
};
|
||||
}
|
||||
|
||||
async function handle(reply: FastifyReply, traceId: string, work: () => Promise<unknown>) {
|
||||
try {
|
||||
return await work();
|
||||
} catch (error) {
|
||||
if (!(error instanceof OrderStateError)) throw error;
|
||||
const status = error.code === 'ORDER_NOT_FOUND' ? 404
|
||||
: error.code === 'ORDER_TRANSITION_NOT_ALLOWED' ? 409
|
||||
: error.code.includes('FORBIDDEN') ? 403 : 400;
|
||||
return reply.status(status).send({
|
||||
code: error.code,
|
||||
message: 'The requested order action is not available.',
|
||||
traceId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function unauthorized(reply: FastifyReply, traceId: string) {
|
||||
return reply.status(401).send({
|
||||
code: 'AUTH_SESSION_INVALID', message: 'Authentication required.', traceId
|
||||
});
|
||||
}
|
||||
|
||||
function invalid(reply: FastifyReply, traceId: string) {
|
||||
return reply.status(400).send({
|
||||
code: 'INVALID_ORDER_ACTION', message: 'The order action is invalid.', traceId
|
||||
});
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { resolve } from 'node:path';
|
||||
import { StoreDiscoveryRepository } from './stores/store-discovery-repository.js';
|
||||
import { StoreAccessRepository } from './stores/access-repository.js';
|
||||
import { PricingRepository } from './orders/pricing-repository.js';
|
||||
import { OrderStateRepository } from './orders/order-state-repository.js';
|
||||
|
||||
const config = loadConfig();
|
||||
const pool = createMySqlPool(config);
|
||||
@@ -63,6 +64,12 @@ const app = await buildApp({
|
||||
authRepository,
|
||||
accessControl,
|
||||
jwtSecret: config.auth.jwtSecret
|
||||
},
|
||||
orderState: {
|
||||
repository: new OrderStateRepository(pool),
|
||||
authRepository,
|
||||
accessControl,
|
||||
jwtSecret: config.auth.jwtSecret
|
||||
}
|
||||
});
|
||||
app.addHook('onClose', async () => {
|
||||
|
||||
@@ -137,7 +137,8 @@ export class StoreAccessRepository {
|
||||
INNER JOIN qipai_orders o
|
||||
ON o.id = a.order_id AND o.tenant_id = a.tenant_id AND o.deleted_at IS NULL
|
||||
WHERE a.tenant_id = ? AND a.user_id = ? AND a.revoked_at IS NULL
|
||||
AND o.store_id = ? AND o.status IN ('PAID', 'CONFIRMED', 'IN_USE')
|
||||
AND o.store_id = ?
|
||||
AND o.status IN ('PAID', 'RESERVED', 'IN_PROGRESS', 'CONFIRMED', 'IN_USE')
|
||||
AND UTC_TIMESTAMP(3) BETWEEN DATE_SUB(o.start_at, INTERVAL 30 MINUTE) AND o.end_at`,
|
||||
[input.tenantId, input.userId, input.storeId]
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user