Service Orders — backend foundation (Task #62)
- New `service_orders` table (status pending|claimed|delivered|received|cancelled) - New `orders:receive` permission + `order_receiver` role; admins implicitly allowed - Added `requirePermission(name)` and `userHasPermission(userId, name)` middleware helpers - New routes: - POST /api/orders place order (authenticated) - GET /api/orders/my list current user's orders - GET /api/orders/incoming receivers see pending+active visible orders - PATCH /api/orders/:id/status claim (atomic), deliver, cancel - PATCH /api/orders/:id/confirm-receipt requester confirms a delivered order - Atomic claim via UPDATE ... WHERE status='pending' AND assigned_to IS NULL (returns 409 already_claimed on race) - Realtime: emits `notification_created` to receivers/requester, `order_incoming_changed` to all receivers, `order_updated` to requester - Service shape in order responses limited to id/nameAr/nameEn/imageUrl (no description fields), per spec - OpenAPI updated with new paths and schemas; codegen run - Seed updated idempotently (permission, role, role_permissions) - New tests in artifacts/api-server/tests/service-orders.test.mjs (full lifecycle, atomic claim race, unauth rejection) — all 21 api tests pass No deviations from the planned scope. Tasks #63 (client UI) and #64 (receiver page + admin role toggle) remain blocked-by #62 and are next.
This commit is contained in:
@@ -2,6 +2,7 @@ export * from "./users";
|
||||
export * from "./roles";
|
||||
export * from "./apps";
|
||||
export * from "./services";
|
||||
export * from "./service-orders";
|
||||
export * from "./conversations";
|
||||
export * from "./notifications";
|
||||
export * from "./settings";
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { pgTable, text, serial, timestamp, integer, varchar } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod/v4";
|
||||
import { usersTable } from "./users";
|
||||
import { servicesTable } from "./services";
|
||||
|
||||
export const serviceOrdersTable = pgTable("service_orders", {
|
||||
id: serial("id").primaryKey(),
|
||||
serviceId: integer("service_id").notNull().references(() => servicesTable.id, { onDelete: "restrict" }),
|
||||
requestedBy: integer("requested_by").notNull().references(() => usersTable.id, { onDelete: "cascade" }),
|
||||
assignedTo: integer("assigned_to").references(() => usersTable.id, { onDelete: "set null" }),
|
||||
quantity: integer("quantity").notNull().default(1),
|
||||
notes: text("notes"),
|
||||
status: varchar("status", { length: 30 }).notNull().default("pending"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
claimedAt: timestamp("claimed_at", { withTimezone: true }),
|
||||
deliveredAt: timestamp("delivered_at", { withTimezone: true }),
|
||||
receivedAt: timestamp("received_at", { withTimezone: true }),
|
||||
cancelledAt: timestamp("cancelled_at", { withTimezone: true }),
|
||||
});
|
||||
|
||||
export const insertServiceOrderSchema = createInsertSchema(serviceOrdersTable).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
claimedAt: true,
|
||||
deliveredAt: true,
|
||||
receivedAt: true,
|
||||
cancelledAt: true,
|
||||
status: true,
|
||||
assignedTo: true,
|
||||
});
|
||||
export type InsertServiceOrder = z.infer<typeof insertServiceOrderSchema>;
|
||||
export type ServiceOrder = typeof serviceOrdersTable.$inferSelect;
|
||||
|
||||
export const SERVICE_ORDER_STATUSES = [
|
||||
"pending",
|
||||
"claimed",
|
||||
"delivered",
|
||||
"received",
|
||||
"cancelled",
|
||||
] as const;
|
||||
export type ServiceOrderStatus = (typeof SERVICE_ORDER_STATUSES)[number];
|
||||
Reference in New Issue
Block a user