@porulle/plugin-pos-restaurant 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +61 -0
  2. package/dist/hooks/modifier-validation.d.ts +21 -0
  3. package/dist/hooks/modifier-validation.d.ts.map +1 -0
  4. package/dist/hooks/modifier-validation.js +40 -0
  5. package/dist/hooks/table-lifecycle.d.ts +15 -0
  6. package/dist/hooks/table-lifecycle.d.ts.map +1 -0
  7. package/dist/hooks/table-lifecycle.js +40 -0
  8. package/dist/index.d.ts +60 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +169 -0
  11. package/dist/routes/kds.d.ts +9 -0
  12. package/dist/routes/kds.d.ts.map +1 -0
  13. package/dist/routes/kds.js +112 -0
  14. package/dist/routes/modifiers.d.ts +15 -0
  15. package/dist/routes/modifiers.d.ts.map +1 -0
  16. package/dist/routes/modifiers.js +117 -0
  17. package/dist/routes/operations.d.ts +34 -0
  18. package/dist/routes/operations.d.ts.map +1 -0
  19. package/dist/routes/operations.js +249 -0
  20. package/dist/routes/tables.d.ts +9 -0
  21. package/dist/routes/tables.d.ts.map +1 -0
  22. package/dist/routes/tables.js +110 -0
  23. package/dist/schema.d.ts +3831 -0
  24. package/dist/schema.d.ts.map +1 -0
  25. package/dist/schema.js +418 -0
  26. package/dist/services/alert-service.d.ts +56 -0
  27. package/dist/services/alert-service.d.ts.map +1 -0
  28. package/dist/services/alert-service.js +173 -0
  29. package/dist/services/analytics-service.d.ts +60 -0
  30. package/dist/services/analytics-service.d.ts.map +1 -0
  31. package/dist/services/analytics-service.js +178 -0
  32. package/dist/services/checklist-service.d.ts +66 -0
  33. package/dist/services/checklist-service.d.ts.map +1 -0
  34. package/dist/services/checklist-service.js +106 -0
  35. package/dist/services/kds-service.d.ts +77 -0
  36. package/dist/services/kds-service.d.ts.map +1 -0
  37. package/dist/services/kds-service.js +246 -0
  38. package/dist/services/modifier-service.d.ts +74 -0
  39. package/dist/services/modifier-service.d.ts.map +1 -0
  40. package/dist/services/modifier-service.js +180 -0
  41. package/dist/services/recipe-deduction-service.d.ts +64 -0
  42. package/dist/services/recipe-deduction-service.d.ts.map +1 -0
  43. package/dist/services/recipe-deduction-service.js +144 -0
  44. package/dist/services/recipe-service.d.ts +43 -0
  45. package/dist/services/recipe-service.d.ts.map +1 -0
  46. package/dist/services/recipe-service.js +85 -0
  47. package/dist/services/table-service.d.ts +61 -0
  48. package/dist/services/table-service.d.ts.map +1 -0
  49. package/dist/services/table-service.js +206 -0
  50. package/dist/types.d.ts +35 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +6 -0
  53. package/package.json +61 -0
  54. package/src/hooks/modifier-validation.ts +60 -0
  55. package/src/hooks/table-lifecycle.ts +50 -0
  56. package/src/index.ts +218 -0
  57. package/src/routes/kds.ts +121 -0
  58. package/src/routes/modifiers.ts +129 -0
  59. package/src/routes/operations.ts +276 -0
  60. package/src/routes/tables.ts +117 -0
  61. package/src/schema.ts +462 -0
  62. package/src/services/alert-service.ts +234 -0
  63. package/src/services/analytics-service.ts +242 -0
  64. package/src/services/checklist-service.ts +139 -0
  65. package/src/services/kds-service.ts +340 -0
  66. package/src/services/modifier-service.ts +251 -0
  67. package/src/services/recipe-deduction-service.ts +221 -0
  68. package/src/services/recipe-service.ts +115 -0
  69. package/src/services/table-service.ts +260 -0
  70. package/src/types.ts +63 -0
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Modifier Validation Hook — cart.beforeAddItem
3
+ *
4
+ * Validates modifier selections when items are added to a POS cart.
5
+ * URY has no modifier validation (modifiers are just flat item links).
6
+ * This hook enforces:
7
+ * - Required modifier groups must have at least minSelect selections
8
+ * - No group may exceed maxSelect selections
9
+ * - Unavailable (86'd) options are rejected
10
+ * - Price adjustments are summed into cart line item metadata
11
+ */
12
+
13
+ import type { ModifierService } from "../services/modifier-service.js";
14
+
15
+ export function buildModifierValidationHook(getService: () => ModifierService) {
16
+ return {
17
+ key: "cart.beforeAddItem",
18
+ handler: async (...args: unknown[]) => {
19
+ const hook = args[0] as {
20
+ data: {
21
+ entityId?: string;
22
+ metadata?: Record<string, unknown>;
23
+ [key: string]: unknown;
24
+ };
25
+ context: {
26
+ actor?: { organizationId?: string | null } | null;
27
+ [key: string]: unknown;
28
+ };
29
+ };
30
+
31
+ const { data, context } = hook;
32
+ const modifiers = data.metadata?.modifiers as Array<{
33
+ groupId: string;
34
+ optionIds: string[];
35
+ }> | undefined;
36
+
37
+ // If no modifiers provided, skip validation (non-restaurant items)
38
+ if (!modifiers || modifiers.length === 0) return data;
39
+ if (!data.entityId) return data;
40
+
41
+ const { resolveOrgId } = await import("@porulle/core");
42
+ const orgId = resolveOrgId(context.actor);
43
+ const service = getService();
44
+
45
+ const result = await service.validateModifiers(orgId, data.entityId, modifiers);
46
+ if (!result.ok) {
47
+ throw new Error(result.error);
48
+ }
49
+
50
+ // Inject validated modifiers and price adjustment into metadata
51
+ data.metadata = {
52
+ ...data.metadata,
53
+ validatedModifiers: result.value.validatedModifiers,
54
+ modifierPriceAdjustment: result.value.totalAdjustment,
55
+ };
56
+
57
+ return data;
58
+ },
59
+ };
60
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Table Lifecycle Hooks
3
+ *
4
+ * URY equivalent: table_status_delete() in ury_pos_invoice.py.
5
+ * URY clears occupied=0 on POS Invoice delete/cancel/submit.
6
+ *
7
+ * We hook into checkout.afterCreate to update table status when a
8
+ * POS transaction completes, and pos transaction void to clear tables.
9
+ */
10
+
11
+ import { eq } from "@porulle/core/drizzle";
12
+ import { posTableAssignments, posTables } from "../schema.js";
13
+ import type { Db } from "../types.js";
14
+
15
+ export function buildTableClearOnCompleteHook(getDb: () => Db) {
16
+ return {
17
+ key: "checkout.afterCreate",
18
+ handler: async (...args: unknown[]) => {
19
+ const hook = args[0] as {
20
+ result: { id: string; metadata?: Record<string, unknown> | null };
21
+ context: { [key: string]: unknown };
22
+ };
23
+
24
+ const metadata = hook.result?.metadata;
25
+ const posTransactionId = (metadata as Record<string, unknown>)?.posTransactionId as string | undefined;
26
+ if (!posTransactionId) return;
27
+
28
+ const db = getDb();
29
+
30
+ // Find table assignments for this transaction
31
+ const assignments = await db
32
+ .select()
33
+ .from(posTableAssignments)
34
+ .where(eq(posTableAssignments.transactionId, posTransactionId));
35
+
36
+ // Set tables to "cleaning" (staff will set to "available" after bussing)
37
+ for (const assignment of assignments) {
38
+ await db
39
+ .update(posTables)
40
+ .set({ status: "cleaning", updatedAt: new Date() })
41
+ .where(eq(posTables.id, assignment.tableId));
42
+ }
43
+
44
+ // Remove assignments
45
+ await db
46
+ .delete(posTableAssignments)
47
+ .where(eq(posTableAssignments.transactionId, posTransactionId));
48
+ },
49
+ };
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1,218 @@
1
+ /**
2
+ * POS Restaurant Extension — RFC-024a (Complete Feature Set)
3
+ *
4
+ * Extends @porulle/plugin-pos with all restaurant-specific features
5
+ * informed by URY Restaurant ERP (ury-erp/ury) production patterns.
6
+ *
7
+ * Feature coverage (maps to URY FEATURES.md):
8
+ *
9
+ * POS & Billing:
10
+ * - Pre-billing checklists (compliance enforcement)
11
+ * - Table service, QSR, and takeaway (order types)
12
+ * - Multi-cashier handling (via Tier 0 terminals)
13
+ * - Shift opening/closing with cash reconciliation (via Tier 0)
14
+ *
15
+ * Menu & Recipe Management:
16
+ * - Item modifiers (groups, options, required/optional, price adjustments)
17
+ * - Recipe/BOM mapping for COGS calculation
18
+ * - Combos and item bundles
19
+ * - Menu availability per outlet (86'd items)
20
+ *
21
+ * Table Order Management:
22
+ * - Table management with zones, floor plan, status lifecycle
23
+ * - Server section assignment
24
+ * - Table/captain transfer
25
+ * - Customer favorites (top ordered items)
26
+ *
27
+ * Kitchen Display & KOT Management:
28
+ * - Multi-station KDS with item-group routing
29
+ * - 4-state ticket flow (pending -> preparing -> ready -> served)
30
+ * - Item-level completion tracking (persistent, not localStorage)
31
+ * - Course sequencing with priority-based ordering
32
+ * - Delay and modification tracking
33
+ * - KOT reprint support
34
+ *
35
+ * Operational Red Flags & Alerts:
36
+ * - Delayed order alerts
37
+ * - KOT not started alerts
38
+ * - Prolonged table occupancy alerts
39
+ * - Excessive cancellation tracking
40
+ * - Configurable thresholds per alert type
41
+ *
42
+ * Reports & Analytics:
43
+ * - Daily Profit & Loss (gross sales, COGS, expenses, net profit)
44
+ * - Station performance (prep time, throughput)
45
+ * - Course-wise performance
46
+ * - Staff/captain performance
47
+ * - P&L expense breakdown
48
+ */
49
+
50
+ import { defineCommercePlugin } from "@porulle/core";
51
+ import {
52
+ posModifierGroups,
53
+ posModifierOptions,
54
+ posTables,
55
+ posTableAssignments,
56
+ kdsStations,
57
+ kdsStationItemGroups,
58
+ kdsTickets,
59
+ kdsTicketItems,
60
+ posChecklists,
61
+ posChecklistItems,
62
+ posChecklistCompletions,
63
+ posRestaurantAlerts,
64
+ posAlertConfig,
65
+ posRecipes,
66
+ posRecipeIngredients,
67
+ posCombos,
68
+ posComboGroups,
69
+ posComboItems,
70
+ posMenuAvailability,
71
+ posDailyPnl,
72
+ posPnlExpenses,
73
+ posCustomerFavorites,
74
+ } from "./schema.js";
75
+ import { ModifierService } from "./services/modifier-service.js";
76
+ import { TableService } from "./services/table-service.js";
77
+ import { KDSService } from "./services/kds-service.js";
78
+ import { ChecklistService } from "./services/checklist-service.js";
79
+ import { AlertService } from "./services/alert-service.js";
80
+ import { RecipeService } from "./services/recipe-service.js";
81
+ import { RestaurantAnalyticsService } from "./services/analytics-service.js";
82
+ import { buildModifierRoutes, buildModifierOptionRoutes } from "./routes/modifiers.js";
83
+ import { buildTableRoutes } from "./routes/tables.js";
84
+ import { buildKDSRoutes } from "./routes/kds.js";
85
+ import {
86
+ buildChecklistRoutes,
87
+ buildAlertRoutes,
88
+ buildRecipeRoutes,
89
+ buildAnalyticsRoutes,
90
+ } from "./routes/operations.js";
91
+ import { buildModifierValidationHook } from "./hooks/modifier-validation.js";
92
+ import { buildTableClearOnCompleteHook } from "./hooks/table-lifecycle.js";
93
+ import type { POSRestaurantPluginOptions, Db } from "./types.js";
94
+ import { DEFAULT_RESTAURANT_OPTIONS } from "./types.js";
95
+
96
+ export type { POSRestaurantPluginOptions, Db } from "./types.js";
97
+ export { ModifierService } from "./services/modifier-service.js";
98
+ export { TableService } from "./services/table-service.js";
99
+ export { KDSService } from "./services/kds-service.js";
100
+ export { ChecklistService } from "./services/checklist-service.js";
101
+ export { AlertService } from "./services/alert-service.js";
102
+ export { RecipeService } from "./services/recipe-service.js";
103
+ export { RestaurantAnalyticsService } from "./services/analytics-service.js";
104
+ export { RecipeDeductionService } from "./services/recipe-deduction-service.js";
105
+
106
+ export function posRestaurantPlugin(userOptions: POSRestaurantPluginOptions = {}) {
107
+ const options: Required<POSRestaurantPluginOptions> = {
108
+ ...DEFAULT_RESTAURANT_OPTIONS,
109
+ ...userOptions,
110
+ };
111
+
112
+ const dbRef: { current: Db | null } = { current: null };
113
+ const modifierServiceRef: { current: ModifierService | null } = { current: null };
114
+
115
+ return defineCommercePlugin({
116
+ id: "pos-restaurant",
117
+ version: "1.0.0",
118
+ requires: ["pos"],
119
+
120
+ permissions: [
121
+ {
122
+ scope: "pos-restaurant:admin",
123
+ description: "Create/edit modifier groups, tables, KDS stations, checklists, recipes, alert config, analytics, floor plan layout.",
124
+ },
125
+ ],
126
+
127
+ schema: () => ({
128
+ // Core restaurant tables
129
+ posModifierGroups,
130
+ posModifierOptions,
131
+ posTables,
132
+ posTableAssignments,
133
+ kdsStations,
134
+ kdsStationItemGroups,
135
+ kdsTickets,
136
+ kdsTicketItems,
137
+ // Operational features
138
+ posChecklists,
139
+ posChecklistItems,
140
+ posChecklistCompletions,
141
+ posRestaurantAlerts,
142
+ posAlertConfig,
143
+ // Menu & recipe management
144
+ posRecipes,
145
+ posRecipeIngredients,
146
+ posCombos,
147
+ posComboGroups,
148
+ posComboItems,
149
+ posMenuAvailability,
150
+ // Analytics
151
+ posDailyPnl,
152
+ posPnlExpenses,
153
+ posCustomerFavorites,
154
+ }),
155
+
156
+ hooks: () => {
157
+ const hooks = [];
158
+
159
+ if (options.enableModifiers) {
160
+ hooks.push(buildModifierValidationHook(() => {
161
+ if (!modifierServiceRef.current) throw new Error("ModifierService not initialized");
162
+ return modifierServiceRef.current;
163
+ }));
164
+ }
165
+
166
+ hooks.push(buildTableClearOnCompleteHook(() => {
167
+ if (!dbRef.current) throw new Error("Restaurant plugin DB not initialized");
168
+ return dbRef.current;
169
+ }));
170
+
171
+ return hooks;
172
+ },
173
+
174
+ routes: (ctx) => {
175
+ const db = ctx.database.db;
176
+ if (!db) return [];
177
+
178
+ dbRef.current = db;
179
+
180
+ // Initialize all services
181
+ const modifierService = new ModifierService(db);
182
+ const tableService = new TableService(db);
183
+ const kdsService = new KDSService(db);
184
+ const checklistService = new ChecklistService(db);
185
+ const alertService = new AlertService(db);
186
+ const recipeService = new RecipeService(db);
187
+ const analyticsService = new RestaurantAnalyticsService(db);
188
+
189
+ modifierServiceRef.current = modifierService;
190
+
191
+ const routes = [
192
+ // Table management (always enabled)
193
+ ...buildTableRoutes(tableService, ctx),
194
+ // Checklists (always enabled)
195
+ ...buildChecklistRoutes(checklistService, ctx),
196
+ // Alerts (always enabled)
197
+ ...buildAlertRoutes(alertService, ctx),
198
+ // Recipes/BOM (always enabled)
199
+ ...buildRecipeRoutes(recipeService, ctx),
200
+ // Analytics (always enabled)
201
+ ...buildAnalyticsRoutes(analyticsService, ctx),
202
+ ];
203
+
204
+ if (options.enableModifiers) {
205
+ routes.push(
206
+ ...buildModifierRoutes(modifierService, ctx),
207
+ ...buildModifierOptionRoutes(modifierService, ctx),
208
+ );
209
+ }
210
+
211
+ if (options.enableKDS) {
212
+ routes.push(...buildKDSRoutes(kdsService, ctx));
213
+ }
214
+
215
+ return routes;
216
+ },
217
+ });
218
+ }
@@ -0,0 +1,121 @@
1
+ import { router } from "@porulle/core";
2
+ import { z } from "@hono/zod-openapi";
3
+ import type { KDSService } from "../services/kds-service.js";
4
+ import type { PluginRouteRegistration } from "@porulle/core";
5
+
6
+ export function buildKDSRoutes(
7
+ service: KDSService,
8
+ ctx: { services?: Record<string, unknown>; database?: { db: unknown } },
9
+ ): PluginRouteRegistration[] {
10
+ const r = router("POS Restaurant KDS", "/pos/restaurant/kds", ctx);
11
+
12
+ // ─── Stations ──────────────────────────────────────────────────────
13
+
14
+ r.post("/stations")
15
+ .summary("Create KDS station")
16
+ .permission("pos-restaurant:admin")
17
+ .input(z.object({
18
+ name: z.string().min(1).max(100),
19
+ alertThresholdMinutes: z.number().int().min(1).optional(),
20
+ metadata: z.record(z.string(), z.unknown()).optional(),
21
+ }))
22
+ .handler(async ({ input, orgId }) => {
23
+ const body = input as { name: string; alertThresholdMinutes?: number; metadata?: Record<string, unknown> };
24
+ const result = await service.createStation(orgId, body);
25
+ if (!result.ok) throw new Error(result.error);
26
+ return result.value;
27
+ });
28
+
29
+ r.get("/stations")
30
+ .summary("List KDS stations")
31
+ .permission("pos:operate")
32
+ .handler(async ({ orgId }) => {
33
+ const result = await service.listStations(orgId);
34
+ if (!result.ok) throw new Error(result.error);
35
+ return result.value;
36
+ });
37
+
38
+ r.patch("/stations/{id}")
39
+ .summary("Update KDS station")
40
+ .permission("pos-restaurant:admin")
41
+ .input(z.object({
42
+ name: z.string().min(1).max(100).optional(),
43
+ isActive: z.boolean().optional(),
44
+ alertThresholdMinutes: z.number().int().min(1).optional(),
45
+ }))
46
+ .handler(async ({ params, input, orgId }) => {
47
+ const body = input as { name?: string; isActive?: boolean; alertThresholdMinutes?: number };
48
+ const result = await service.updateStation(orgId, params.id!, body);
49
+ if (!result.ok) throw new Error(result.error);
50
+ return result.value;
51
+ });
52
+
53
+ r.post("/stations/{id}/item-groups")
54
+ .summary("Add item group to station")
55
+ .permission("pos-restaurant:admin")
56
+ .input(z.object({ itemGroup: z.string().min(1) }))
57
+ .handler(async ({ params, input }) => {
58
+ const body = input as { itemGroup: string };
59
+ const result = await service.addItemGroup(params.id!, body.itemGroup);
60
+ if (!result.ok) throw new Error(result.error);
61
+ return result.value;
62
+ });
63
+
64
+ r.delete("/stations/{id}/item-groups/{group}")
65
+ .summary("Remove item group from station")
66
+ .permission("pos-restaurant:admin")
67
+ .handler(async ({ params }) => {
68
+ const result = await service.removeItemGroup(params.id!, params.group!);
69
+ if (!result.ok) throw new Error(result.error);
70
+ return result.value;
71
+ });
72
+
73
+ // ─── Tickets ───────────────────────────────────────────────────────
74
+
75
+ r.get("/stations/{id}/tickets")
76
+ .summary("List pending tickets for station")
77
+ .permission("pos:operate")
78
+ .handler(async ({ params, orgId }) => {
79
+ const result = await service.listPendingTickets(orgId, params.id!);
80
+ if (!result.ok) throw new Error(result.error);
81
+ return result.value;
82
+ });
83
+
84
+ r.post("/tickets/{id}/start")
85
+ .summary("Mark ticket as preparing")
86
+ .permission("pos:operate")
87
+ .handler(async ({ params }) => {
88
+ const result = await service.startTicket(params.id!);
89
+ if (!result.ok) throw new Error(result.error);
90
+ return result.value;
91
+ });
92
+
93
+ r.post("/tickets/{id}/ready")
94
+ .summary("Mark ticket as ready")
95
+ .permission("pos:operate")
96
+ .handler(async ({ params }) => {
97
+ const result = await service.readyTicket(params.id!);
98
+ if (!result.ok) throw new Error(result.error);
99
+ return result.value;
100
+ });
101
+
102
+ r.post("/tickets/{id}/serve")
103
+ .summary("Mark ticket as served")
104
+ .permission("pos:operate")
105
+ .handler(async ({ params }) => {
106
+ const result = await service.serveTicket(params.id!);
107
+ if (!result.ok) throw new Error(result.error);
108
+ return result.value;
109
+ });
110
+
111
+ r.post("/tickets/{id}/items/{itemId}/done")
112
+ .summary("Mark ticket item as done")
113
+ .permission("pos:operate")
114
+ .handler(async ({ params }) => {
115
+ const result = await service.markItemDone(params.id!, params.itemId!);
116
+ if (!result.ok) throw new Error(result.error);
117
+ return result.value;
118
+ });
119
+
120
+ return r.routes();
121
+ }
@@ -0,0 +1,129 @@
1
+ import { router } from "@porulle/core";
2
+ import { z } from "@hono/zod-openapi";
3
+ import type { ModifierService } from "../services/modifier-service.js";
4
+ import type { PluginRouteRegistration } from "@porulle/core";
5
+
6
+ export function buildModifierRoutes(
7
+ service: ModifierService,
8
+ ctx: { services?: Record<string, unknown>; database?: { db: unknown } },
9
+ ): PluginRouteRegistration[] {
10
+ const r = router("POS Restaurant Modifiers", "/pos/restaurant/modifier-groups", ctx);
11
+
12
+ r.post("/")
13
+ .summary("Create modifier group")
14
+ .permission("pos-restaurant:admin")
15
+ .input(z.object({
16
+ name: z.string().min(1).max(200),
17
+ entityId: z.string().uuid().optional(),
18
+ itemGroup: z.string().optional(),
19
+ isRequired: z.boolean().optional(),
20
+ minSelect: z.number().int().min(0).optional(),
21
+ maxSelect: z.number().int().min(1).optional(),
22
+ sortOrder: z.number().int().optional(),
23
+ }))
24
+ .handler(async ({ input, orgId }) => {
25
+ const body = input as { name: string; entityId?: string; itemGroup?: string; isRequired?: boolean; minSelect?: number; maxSelect?: number; sortOrder?: number };
26
+ const result = await service.createGroup(orgId, body);
27
+ if (!result.ok) throw new Error(result.error);
28
+ return result.value;
29
+ });
30
+
31
+ r.get("/")
32
+ .summary("List modifier groups")
33
+ .permission("pos:operate")
34
+ .query(z.object({ entityId: z.string().uuid().optional() }))
35
+ .handler(async ({ query, orgId }) => {
36
+ const q = query as { entityId?: string };
37
+ const result = await service.listGroups(orgId, q.entityId);
38
+ if (!result.ok) throw new Error(result.error);
39
+ return result.value;
40
+ });
41
+
42
+ r.get("/{id}")
43
+ .summary("Get modifier group with options")
44
+ .permission("pos:operate")
45
+ .handler(async ({ params, orgId }) => {
46
+ const result = await service.getGroupWithOptions(orgId, params.id!);
47
+ if (!result.ok) throw new Error(result.error);
48
+ return result.value;
49
+ });
50
+
51
+ r.patch("/{id}")
52
+ .summary("Update modifier group")
53
+ .permission("pos-restaurant:admin")
54
+ .input(z.object({
55
+ name: z.string().min(1).max(200).optional(),
56
+ isRequired: z.boolean().optional(),
57
+ minSelect: z.number().int().min(0).optional(),
58
+ maxSelect: z.number().int().min(1).optional(),
59
+ sortOrder: z.number().int().optional(),
60
+ }))
61
+ .handler(async ({ params, input, orgId }) => {
62
+ const body = input as { name?: string; isRequired?: boolean; minSelect?: number; maxSelect?: number; sortOrder?: number };
63
+ const result = await service.updateGroup(orgId, params.id!, body);
64
+ if (!result.ok) throw new Error(result.error);
65
+ return result.value;
66
+ });
67
+
68
+ r.delete("/{id}")
69
+ .summary("Delete modifier group")
70
+ .permission("pos-restaurant:admin")
71
+ .handler(async ({ params, orgId }) => {
72
+ const result = await service.deleteGroup(orgId, params.id!);
73
+ if (!result.ok) throw new Error(result.error);
74
+ return result.value;
75
+ });
76
+
77
+ r.post("/{id}/options")
78
+ .summary("Add modifier option")
79
+ .permission("pos-restaurant:admin")
80
+ .input(z.object({
81
+ name: z.string().min(1).max(200),
82
+ priceAdjustment: z.number().int().optional(),
83
+ isDefault: z.boolean().optional(),
84
+ sortOrder: z.number().int().optional(),
85
+ }))
86
+ .handler(async ({ params, input }) => {
87
+ const body = input as { name: string; priceAdjustment?: number; isDefault?: boolean; sortOrder?: number };
88
+ const result = await service.addOption(params.id!, body);
89
+ if (!result.ok) throw new Error(result.error);
90
+ return result.value;
91
+ });
92
+
93
+ return r.routes();
94
+ }
95
+
96
+ export function buildModifierOptionRoutes(
97
+ service: ModifierService,
98
+ ctx: { services?: Record<string, unknown>; database?: { db: unknown } },
99
+ ): PluginRouteRegistration[] {
100
+ const r = router("POS Restaurant Modifier Options", "/pos/restaurant/modifier-options", ctx);
101
+
102
+ r.patch("/{id}")
103
+ .summary("Update modifier option")
104
+ .permission("pos-restaurant:admin")
105
+ .input(z.object({
106
+ name: z.string().min(1).max(200).optional(),
107
+ priceAdjustment: z.number().int().optional(),
108
+ isDefault: z.boolean().optional(),
109
+ isAvailable: z.boolean().optional(),
110
+ sortOrder: z.number().int().optional(),
111
+ }))
112
+ .handler(async ({ params, input }) => {
113
+ const body = input as { name?: string; priceAdjustment?: number; isDefault?: boolean; isAvailable?: boolean; sortOrder?: number };
114
+ const result = await service.updateOption(params.id!, body);
115
+ if (!result.ok) throw new Error(result.error);
116
+ return result.value;
117
+ });
118
+
119
+ r.delete("/{id}")
120
+ .summary("Delete modifier option")
121
+ .permission("pos-restaurant:admin")
122
+ .handler(async ({ params }) => {
123
+ const result = await service.deleteOption(params.id!);
124
+ if (!result.ok) throw new Error(result.error);
125
+ return result.value;
126
+ });
127
+
128
+ return r.routes();
129
+ }