@porulle/plugin-scheduled-orders 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.
- package/README.md +54 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/routes/scheduled-orders.d.ts +9 -0
- package/dist/routes/scheduled-orders.d.ts.map +1 -0
- package/dist/routes/scheduled-orders.js +55 -0
- package/dist/schema.d.ts +229 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +20 -0
- package/dist/services/scheduled-order-service.d.ts +22 -0
- package/dist/services/scheduled-order-service.d.ts.map +1 -0
- package/dist/services/scheduled-order-service.js +94 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +55 -0
- package/src/index.ts +27 -0
- package/src/routes/scheduled-orders.ts +67 -0
- package/src/schema.ts +21 -0
- package/src/services/scheduled-order-service.ts +110 -0
- package/src/types.ts +6 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @porulle/plugin-scheduled-orders
|
|
2
|
+
|
|
3
|
+
Defer order execution: schedule future placement and process due schedules in batch.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @porulle/plugin-scheduled-orders
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Add to `commerce.config.ts`:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { scheduledOrdersPlugin } from "@porulle/plugin-scheduled-orders";
|
|
15
|
+
|
|
16
|
+
export default defineConfig({
|
|
17
|
+
plugins: [scheduledOrdersPlugin()],
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Add to `drizzle.config.ts`:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
schema: [
|
|
25
|
+
"./node_modules/@porulle/plugin-scheduled-orders/src/schema.ts",
|
|
26
|
+
// ...
|
|
27
|
+
],
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## What it does
|
|
31
|
+
|
|
32
|
+
Persists scheduled orders with `scheduledFor`, cart linkage, and status (`scheduled`, `processing`, `completed`, `cancelled`, `expired`); lists and cancels them and exposes an admin endpoint to process due rows.
|
|
33
|
+
|
|
34
|
+
## Routes exposed
|
|
35
|
+
|
|
36
|
+
**`/scheduled-orders`** — `POST /`, `GET /`, `GET /{id}`, `POST /{id}/cancel`, `POST /process-due`
|
|
37
|
+
|
|
38
|
+
## Hooks
|
|
39
|
+
|
|
40
|
+
**Emitted:** none.
|
|
41
|
+
|
|
42
|
+
**Consumed:** none.
|
|
43
|
+
|
|
44
|
+
## MCP tools
|
|
45
|
+
|
|
46
|
+
None.
|
|
47
|
+
|
|
48
|
+
## Configuration options
|
|
49
|
+
|
|
50
|
+
None (`scheduledOrdersPlugin()` takes no options).
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,YAAY,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAE9E,wBAAgB,qBAAqB,2CAiBpC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineCommercePlugin } from "@porulle/core";
|
|
2
|
+
import { scheduledOrders } from "./schema.js";
|
|
3
|
+
import { ScheduledOrderService } from "./services/scheduled-order-service.js";
|
|
4
|
+
import { buildScheduledOrderRoutes } from "./routes/scheduled-orders.js";
|
|
5
|
+
export { ScheduledOrderService } from "./services/scheduled-order-service.js";
|
|
6
|
+
export function scheduledOrdersPlugin() {
|
|
7
|
+
return defineCommercePlugin({
|
|
8
|
+
id: "scheduled-orders",
|
|
9
|
+
version: "1.0.0",
|
|
10
|
+
permissions: [
|
|
11
|
+
{ scope: "scheduled-orders:admin", description: "Process due scheduled orders and admin operations." },
|
|
12
|
+
{ scope: "scheduled-orders:create", description: "Create and cancel scheduled orders." },
|
|
13
|
+
{ scope: "scheduled-orders:read", description: "View scheduled orders." },
|
|
14
|
+
],
|
|
15
|
+
schema: () => ({ scheduledOrders }),
|
|
16
|
+
hooks: () => [],
|
|
17
|
+
routes: (ctx) => {
|
|
18
|
+
const db = ctx.database.db;
|
|
19
|
+
if (!db)
|
|
20
|
+
return [];
|
|
21
|
+
return buildScheduledOrderRoutes(new ScheduledOrderService(db), ctx);
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ScheduledOrderService } from "../services/scheduled-order-service.js";
|
|
2
|
+
import type { PluginRouteRegistration } from "@porulle/core";
|
|
3
|
+
export declare function buildScheduledOrderRoutes(service: ScheduledOrderService, ctx: {
|
|
4
|
+
services?: Record<string, unknown>;
|
|
5
|
+
database?: {
|
|
6
|
+
db: unknown;
|
|
7
|
+
};
|
|
8
|
+
}): PluginRouteRegistration[];
|
|
9
|
+
//# sourceMappingURL=scheduled-orders.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduled-orders.d.ts","sourceRoot":"","sources":["../../src/routes/scheduled-orders.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AACpF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAG7D,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,qBAAqB,EAC9B,GAAG,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,QAAQ,CAAC,EAAE;QAAE,EAAE,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GACtE,uBAAuB,EAAE,CAyD3B"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { router } from "@porulle/core";
|
|
2
|
+
import { z } from "@hono/zod-openapi";
|
|
3
|
+
export function buildScheduledOrderRoutes(service, ctx) {
|
|
4
|
+
const r = router("Scheduled Orders", "/scheduled-orders", ctx);
|
|
5
|
+
r.post("/").summary("Create scheduled order").permission("scheduled-orders:create")
|
|
6
|
+
.input(z.object({
|
|
7
|
+
customerId: z.string().uuid(),
|
|
8
|
+
cartId: z.string().uuid(),
|
|
9
|
+
scheduledFor: z.string().datetime(),
|
|
10
|
+
orderType: z.enum(["pickup", "delivery", "dine_in"]).optional(),
|
|
11
|
+
pickupLocation: z.string().optional(),
|
|
12
|
+
deliveryAddress: z.any().optional(),
|
|
13
|
+
notes: z.string().optional(),
|
|
14
|
+
}))
|
|
15
|
+
.handler(async ({ input, orgId }) => {
|
|
16
|
+
const body = input;
|
|
17
|
+
const result = await service.create(orgId, body);
|
|
18
|
+
if (!result.ok)
|
|
19
|
+
throw new Error(result.error);
|
|
20
|
+
return result.value;
|
|
21
|
+
});
|
|
22
|
+
r.get("/").summary("List scheduled orders").permission("scheduled-orders:read")
|
|
23
|
+
.query(z.object({ status: z.enum(["scheduled", "processing", "completed", "cancelled", "expired"]).optional() }))
|
|
24
|
+
.handler(async ({ query, orgId }) => {
|
|
25
|
+
const q = query;
|
|
26
|
+
const result = await service.list(orgId, q.status);
|
|
27
|
+
if (!result.ok)
|
|
28
|
+
throw new Error(result.error);
|
|
29
|
+
return result.value;
|
|
30
|
+
});
|
|
31
|
+
r.get("/{id}").summary("Get scheduled order by ID").permission("scheduled-orders:read")
|
|
32
|
+
.handler(async ({ params, orgId }) => {
|
|
33
|
+
const result = await service.getById(orgId, params.id);
|
|
34
|
+
if (!result.ok)
|
|
35
|
+
throw new Error(result.error);
|
|
36
|
+
return result.value;
|
|
37
|
+
});
|
|
38
|
+
r.post("/{id}/cancel").summary("Cancel scheduled order").permission("scheduled-orders:create")
|
|
39
|
+
.handler(async ({ params, orgId }) => {
|
|
40
|
+
const result = await service.cancel(orgId, params.id);
|
|
41
|
+
if (!result.ok)
|
|
42
|
+
throw new Error(result.error);
|
|
43
|
+
return result.value;
|
|
44
|
+
});
|
|
45
|
+
r.post("/process-due").summary("Process due scheduled orders").permission("scheduled-orders:admin")
|
|
46
|
+
.input(z.object({ bufferMinutes: z.number().int().min(0).optional() }))
|
|
47
|
+
.handler(async ({ input, orgId }) => {
|
|
48
|
+
const body = input;
|
|
49
|
+
const result = await service.processDue(orgId, body.bufferMinutes);
|
|
50
|
+
if (!result.ok)
|
|
51
|
+
throw new Error(result.error);
|
|
52
|
+
return result.value;
|
|
53
|
+
});
|
|
54
|
+
return r.routes();
|
|
55
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
export declare const scheduledOrders: import("drizzle-orm/pg-core/table").PgTableWithColumns<{
|
|
2
|
+
name: "scheduled_orders";
|
|
3
|
+
schema: undefined;
|
|
4
|
+
columns: {
|
|
5
|
+
id: import("@porulle/core/drizzle").PgColumn<{
|
|
6
|
+
name: "id";
|
|
7
|
+
tableName: "scheduled_orders";
|
|
8
|
+
dataType: "string";
|
|
9
|
+
columnType: "PgUUID";
|
|
10
|
+
data: string;
|
|
11
|
+
driverParam: string;
|
|
12
|
+
notNull: true;
|
|
13
|
+
hasDefault: true;
|
|
14
|
+
isPrimaryKey: true;
|
|
15
|
+
isAutoincrement: false;
|
|
16
|
+
hasRuntimeDefault: false;
|
|
17
|
+
enumValues: undefined;
|
|
18
|
+
baseColumn: never;
|
|
19
|
+
identity: undefined;
|
|
20
|
+
generated: undefined;
|
|
21
|
+
}, {}, {}>;
|
|
22
|
+
organizationId: import("@porulle/core/drizzle").PgColumn<{
|
|
23
|
+
name: "organization_id";
|
|
24
|
+
tableName: "scheduled_orders";
|
|
25
|
+
dataType: "string";
|
|
26
|
+
columnType: "PgText";
|
|
27
|
+
data: string;
|
|
28
|
+
driverParam: string;
|
|
29
|
+
notNull: true;
|
|
30
|
+
hasDefault: false;
|
|
31
|
+
isPrimaryKey: false;
|
|
32
|
+
isAutoincrement: false;
|
|
33
|
+
hasRuntimeDefault: false;
|
|
34
|
+
enumValues: [string, ...string[]];
|
|
35
|
+
baseColumn: never;
|
|
36
|
+
identity: undefined;
|
|
37
|
+
generated: undefined;
|
|
38
|
+
}, {}, {}>;
|
|
39
|
+
customerId: import("@porulle/core/drizzle").PgColumn<{
|
|
40
|
+
name: "customer_id";
|
|
41
|
+
tableName: "scheduled_orders";
|
|
42
|
+
dataType: "string";
|
|
43
|
+
columnType: "PgUUID";
|
|
44
|
+
data: string;
|
|
45
|
+
driverParam: string;
|
|
46
|
+
notNull: true;
|
|
47
|
+
hasDefault: false;
|
|
48
|
+
isPrimaryKey: false;
|
|
49
|
+
isAutoincrement: false;
|
|
50
|
+
hasRuntimeDefault: false;
|
|
51
|
+
enumValues: undefined;
|
|
52
|
+
baseColumn: never;
|
|
53
|
+
identity: undefined;
|
|
54
|
+
generated: undefined;
|
|
55
|
+
}, {}, {}>;
|
|
56
|
+
cartId: import("@porulle/core/drizzle").PgColumn<{
|
|
57
|
+
name: "cart_id";
|
|
58
|
+
tableName: "scheduled_orders";
|
|
59
|
+
dataType: "string";
|
|
60
|
+
columnType: "PgUUID";
|
|
61
|
+
data: string;
|
|
62
|
+
driverParam: string;
|
|
63
|
+
notNull: true;
|
|
64
|
+
hasDefault: false;
|
|
65
|
+
isPrimaryKey: false;
|
|
66
|
+
isAutoincrement: false;
|
|
67
|
+
hasRuntimeDefault: false;
|
|
68
|
+
enumValues: undefined;
|
|
69
|
+
baseColumn: never;
|
|
70
|
+
identity: undefined;
|
|
71
|
+
generated: undefined;
|
|
72
|
+
}, {}, {}>;
|
|
73
|
+
scheduledFor: import("@porulle/core/drizzle").PgColumn<{
|
|
74
|
+
name: "scheduled_for";
|
|
75
|
+
tableName: "scheduled_orders";
|
|
76
|
+
dataType: "date";
|
|
77
|
+
columnType: "PgTimestamp";
|
|
78
|
+
data: Date;
|
|
79
|
+
driverParam: string;
|
|
80
|
+
notNull: true;
|
|
81
|
+
hasDefault: false;
|
|
82
|
+
isPrimaryKey: false;
|
|
83
|
+
isAutoincrement: false;
|
|
84
|
+
hasRuntimeDefault: false;
|
|
85
|
+
enumValues: undefined;
|
|
86
|
+
baseColumn: never;
|
|
87
|
+
identity: undefined;
|
|
88
|
+
generated: undefined;
|
|
89
|
+
}, {}, {}>;
|
|
90
|
+
orderType: import("@porulle/core/drizzle").PgColumn<{
|
|
91
|
+
name: "order_type";
|
|
92
|
+
tableName: "scheduled_orders";
|
|
93
|
+
dataType: "string";
|
|
94
|
+
columnType: "PgText";
|
|
95
|
+
data: "pickup" | "delivery" | "dine_in";
|
|
96
|
+
driverParam: string;
|
|
97
|
+
notNull: true;
|
|
98
|
+
hasDefault: true;
|
|
99
|
+
isPrimaryKey: false;
|
|
100
|
+
isAutoincrement: false;
|
|
101
|
+
hasRuntimeDefault: false;
|
|
102
|
+
enumValues: ["pickup", "delivery", "dine_in"];
|
|
103
|
+
baseColumn: never;
|
|
104
|
+
identity: undefined;
|
|
105
|
+
generated: undefined;
|
|
106
|
+
}, {}, {}>;
|
|
107
|
+
status: import("@porulle/core/drizzle").PgColumn<{
|
|
108
|
+
name: "status";
|
|
109
|
+
tableName: "scheduled_orders";
|
|
110
|
+
dataType: "string";
|
|
111
|
+
columnType: "PgText";
|
|
112
|
+
data: "scheduled" | "processing" | "completed" | "cancelled" | "expired";
|
|
113
|
+
driverParam: string;
|
|
114
|
+
notNull: true;
|
|
115
|
+
hasDefault: true;
|
|
116
|
+
isPrimaryKey: false;
|
|
117
|
+
isAutoincrement: false;
|
|
118
|
+
hasRuntimeDefault: false;
|
|
119
|
+
enumValues: ["scheduled", "processing", "completed", "cancelled", "expired"];
|
|
120
|
+
baseColumn: never;
|
|
121
|
+
identity: undefined;
|
|
122
|
+
generated: undefined;
|
|
123
|
+
}, {}, {}>;
|
|
124
|
+
pickupLocation: import("@porulle/core/drizzle").PgColumn<{
|
|
125
|
+
name: "pickup_location";
|
|
126
|
+
tableName: "scheduled_orders";
|
|
127
|
+
dataType: "string";
|
|
128
|
+
columnType: "PgText";
|
|
129
|
+
data: string;
|
|
130
|
+
driverParam: string;
|
|
131
|
+
notNull: false;
|
|
132
|
+
hasDefault: false;
|
|
133
|
+
isPrimaryKey: false;
|
|
134
|
+
isAutoincrement: false;
|
|
135
|
+
hasRuntimeDefault: false;
|
|
136
|
+
enumValues: [string, ...string[]];
|
|
137
|
+
baseColumn: never;
|
|
138
|
+
identity: undefined;
|
|
139
|
+
generated: undefined;
|
|
140
|
+
}, {}, {}>;
|
|
141
|
+
deliveryAddress: import("@porulle/core/drizzle").PgColumn<{
|
|
142
|
+
name: "delivery_address";
|
|
143
|
+
tableName: "scheduled_orders";
|
|
144
|
+
dataType: "json";
|
|
145
|
+
columnType: "PgJsonb";
|
|
146
|
+
data: unknown;
|
|
147
|
+
driverParam: unknown;
|
|
148
|
+
notNull: false;
|
|
149
|
+
hasDefault: false;
|
|
150
|
+
isPrimaryKey: false;
|
|
151
|
+
isAutoincrement: false;
|
|
152
|
+
hasRuntimeDefault: false;
|
|
153
|
+
enumValues: undefined;
|
|
154
|
+
baseColumn: never;
|
|
155
|
+
identity: undefined;
|
|
156
|
+
generated: undefined;
|
|
157
|
+
}, {}, {}>;
|
|
158
|
+
notes: import("@porulle/core/drizzle").PgColumn<{
|
|
159
|
+
name: "notes";
|
|
160
|
+
tableName: "scheduled_orders";
|
|
161
|
+
dataType: "string";
|
|
162
|
+
columnType: "PgText";
|
|
163
|
+
data: string;
|
|
164
|
+
driverParam: string;
|
|
165
|
+
notNull: false;
|
|
166
|
+
hasDefault: false;
|
|
167
|
+
isPrimaryKey: false;
|
|
168
|
+
isAutoincrement: false;
|
|
169
|
+
hasRuntimeDefault: false;
|
|
170
|
+
enumValues: [string, ...string[]];
|
|
171
|
+
baseColumn: never;
|
|
172
|
+
identity: undefined;
|
|
173
|
+
generated: undefined;
|
|
174
|
+
}, {}, {}>;
|
|
175
|
+
processedAt: import("@porulle/core/drizzle").PgColumn<{
|
|
176
|
+
name: "processed_at";
|
|
177
|
+
tableName: "scheduled_orders";
|
|
178
|
+
dataType: "date";
|
|
179
|
+
columnType: "PgTimestamp";
|
|
180
|
+
data: Date;
|
|
181
|
+
driverParam: string;
|
|
182
|
+
notNull: false;
|
|
183
|
+
hasDefault: false;
|
|
184
|
+
isPrimaryKey: false;
|
|
185
|
+
isAutoincrement: false;
|
|
186
|
+
hasRuntimeDefault: false;
|
|
187
|
+
enumValues: undefined;
|
|
188
|
+
baseColumn: never;
|
|
189
|
+
identity: undefined;
|
|
190
|
+
generated: undefined;
|
|
191
|
+
}, {}, {}>;
|
|
192
|
+
createdAt: import("@porulle/core/drizzle").PgColumn<{
|
|
193
|
+
name: "created_at";
|
|
194
|
+
tableName: "scheduled_orders";
|
|
195
|
+
dataType: "date";
|
|
196
|
+
columnType: "PgTimestamp";
|
|
197
|
+
data: Date;
|
|
198
|
+
driverParam: string;
|
|
199
|
+
notNull: true;
|
|
200
|
+
hasDefault: true;
|
|
201
|
+
isPrimaryKey: false;
|
|
202
|
+
isAutoincrement: false;
|
|
203
|
+
hasRuntimeDefault: false;
|
|
204
|
+
enumValues: undefined;
|
|
205
|
+
baseColumn: never;
|
|
206
|
+
identity: undefined;
|
|
207
|
+
generated: undefined;
|
|
208
|
+
}, {}, {}>;
|
|
209
|
+
updatedAt: import("@porulle/core/drizzle").PgColumn<{
|
|
210
|
+
name: "updated_at";
|
|
211
|
+
tableName: "scheduled_orders";
|
|
212
|
+
dataType: "date";
|
|
213
|
+
columnType: "PgTimestamp";
|
|
214
|
+
data: Date;
|
|
215
|
+
driverParam: string;
|
|
216
|
+
notNull: true;
|
|
217
|
+
hasDefault: true;
|
|
218
|
+
isPrimaryKey: false;
|
|
219
|
+
isAutoincrement: false;
|
|
220
|
+
hasRuntimeDefault: false;
|
|
221
|
+
enumValues: undefined;
|
|
222
|
+
baseColumn: never;
|
|
223
|
+
identity: undefined;
|
|
224
|
+
generated: undefined;
|
|
225
|
+
}, {}, {}>;
|
|
226
|
+
};
|
|
227
|
+
dialect: "pg";
|
|
228
|
+
}>;
|
|
229
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkBzB,CAAC"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { pgTable, uuid, text, timestamp, index, jsonb } from "@porulle/core/drizzle";
|
|
2
|
+
export const scheduledOrders = pgTable("scheduled_orders", {
|
|
3
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
4
|
+
organizationId: text("organization_id").notNull(),
|
|
5
|
+
customerId: uuid("customer_id").notNull(),
|
|
6
|
+
cartId: uuid("cart_id").notNull(),
|
|
7
|
+
scheduledFor: timestamp("scheduled_for", { withTimezone: true }).notNull(),
|
|
8
|
+
orderType: text("order_type", { enum: ["pickup", "delivery", "dine_in"] }).notNull().default("pickup"),
|
|
9
|
+
status: text("status", { enum: ["scheduled", "processing", "completed", "cancelled", "expired"] }).notNull().default("scheduled"),
|
|
10
|
+
pickupLocation: text("pickup_location"),
|
|
11
|
+
deliveryAddress: jsonb("delivery_address"),
|
|
12
|
+
notes: text("notes"),
|
|
13
|
+
processedAt: timestamp("processed_at", { withTimezone: true }),
|
|
14
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
15
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
16
|
+
}, (table) => ({
|
|
17
|
+
orgIdx: index("idx_scheduled_orders_org").on(table.organizationId),
|
|
18
|
+
statusIdx: index("idx_scheduled_orders_status").on(table.status),
|
|
19
|
+
scheduledForIdx: index("idx_scheduled_orders_scheduled_for").on(table.scheduledFor),
|
|
20
|
+
}));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PluginResult } from "@porulle/core";
|
|
2
|
+
import type { Db, ScheduledOrder, ScheduledOrderStatus, ScheduledOrderType } from "../types.js";
|
|
3
|
+
export declare class ScheduledOrderService {
|
|
4
|
+
private db;
|
|
5
|
+
constructor(db: Db);
|
|
6
|
+
create(orgId: string, input: {
|
|
7
|
+
customerId: string;
|
|
8
|
+
cartId: string;
|
|
9
|
+
scheduledFor: string;
|
|
10
|
+
orderType?: ScheduledOrderType;
|
|
11
|
+
pickupLocation?: string;
|
|
12
|
+
deliveryAddress?: unknown;
|
|
13
|
+
notes?: string;
|
|
14
|
+
}): Promise<PluginResult<ScheduledOrder>>;
|
|
15
|
+
list(orgId: string, status?: ScheduledOrderStatus): Promise<PluginResult<ScheduledOrder[]>>;
|
|
16
|
+
getById(orgId: string, id: string): Promise<PluginResult<ScheduledOrder>>;
|
|
17
|
+
cancel(orgId: string, id: string): Promise<PluginResult<ScheduledOrder>>;
|
|
18
|
+
processDue(orgId: string, bufferMinutes?: number): Promise<PluginResult<ScheduledOrder[]>>;
|
|
19
|
+
complete(orgId: string, id: string): Promise<PluginResult<ScheduledOrder>>;
|
|
20
|
+
expireOld(orgId: string, hoursThreshold?: number): Promise<PluginResult<ScheduledOrder[]>>;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=scheduled-order-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduled-order-service.d.ts","sourceRoot":"","sources":["../../src/services/scheduled-order-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,KAAK,EAAE,EAAE,EAAE,cAAc,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEhG,qBAAa,qBAAqB;IACpB,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAEpB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QACjC,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;QACf,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,CAAC,EAAE,kBAAkB,CAAC;QAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,eAAe,CAAC,EAAE,OAAO,CAAC;QAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GAAG,OAAO,CAAC,YAAY,CAAC,cAAc,CAAC,CAAC;IAcnC,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC,CAAC;IAO3F,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,cAAc,CAAC,CAAC;IAOzE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,cAAc,CAAC,CAAC;IAYxE,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,GAAE,MAAW,GAAG,OAAO,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC,CAAC;IAsB9F,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,cAAc,CAAC,CAAC;IAW1E,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,GAAE,MAAW,GAAG,OAAO,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC,CAAC;CAmBrG"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { eq, and, lte, sql } from "@porulle/core/drizzle";
|
|
2
|
+
import { Ok, Err } from "@porulle/core";
|
|
3
|
+
import { scheduledOrders } from "../schema.js";
|
|
4
|
+
export class ScheduledOrderService {
|
|
5
|
+
db;
|
|
6
|
+
constructor(db) {
|
|
7
|
+
this.db = db;
|
|
8
|
+
}
|
|
9
|
+
async create(orgId, input) {
|
|
10
|
+
const rows = await this.db.insert(scheduledOrders).values({
|
|
11
|
+
organizationId: orgId,
|
|
12
|
+
customerId: input.customerId,
|
|
13
|
+
cartId: input.cartId,
|
|
14
|
+
scheduledFor: new Date(input.scheduledFor),
|
|
15
|
+
orderType: input.orderType ?? "pickup",
|
|
16
|
+
pickupLocation: input.pickupLocation,
|
|
17
|
+
deliveryAddress: input.deliveryAddress,
|
|
18
|
+
notes: input.notes,
|
|
19
|
+
}).returning();
|
|
20
|
+
return Ok(rows[0]);
|
|
21
|
+
}
|
|
22
|
+
async list(orgId, status) {
|
|
23
|
+
const conditions = [eq(scheduledOrders.organizationId, orgId)];
|
|
24
|
+
if (status)
|
|
25
|
+
conditions.push(eq(scheduledOrders.status, status));
|
|
26
|
+
const rows = await this.db.select().from(scheduledOrders).where(and(...conditions));
|
|
27
|
+
return Ok(rows);
|
|
28
|
+
}
|
|
29
|
+
async getById(orgId, id) {
|
|
30
|
+
const rows = await this.db.select().from(scheduledOrders)
|
|
31
|
+
.where(and(eq(scheduledOrders.organizationId, orgId), eq(scheduledOrders.id, id)));
|
|
32
|
+
if (rows.length === 0)
|
|
33
|
+
return Err("Scheduled order not found");
|
|
34
|
+
return Ok(rows[0]);
|
|
35
|
+
}
|
|
36
|
+
async cancel(orgId, id) {
|
|
37
|
+
const existing = await this.db.select().from(scheduledOrders)
|
|
38
|
+
.where(and(eq(scheduledOrders.organizationId, orgId), eq(scheduledOrders.id, id)));
|
|
39
|
+
if (existing.length === 0)
|
|
40
|
+
return Err("Scheduled order not found");
|
|
41
|
+
if (existing[0].status !== "scheduled")
|
|
42
|
+
return Err(`Cannot cancel order with status '${existing[0].status}'`);
|
|
43
|
+
const rows = await this.db.update(scheduledOrders).set({
|
|
44
|
+
status: "cancelled",
|
|
45
|
+
updatedAt: new Date(),
|
|
46
|
+
}).where(eq(scheduledOrders.id, id)).returning();
|
|
47
|
+
return Ok(rows[0]);
|
|
48
|
+
}
|
|
49
|
+
async processDue(orgId, bufferMinutes = 15) {
|
|
50
|
+
const cutoff = new Date(Date.now() + bufferMinutes * 60 * 1000).toISOString();
|
|
51
|
+
const due = await this.db.select().from(scheduledOrders)
|
|
52
|
+
.where(and(eq(scheduledOrders.organizationId, orgId), eq(scheduledOrders.status, "scheduled"), lte(scheduledOrders.scheduledFor, sql `${cutoff}::timestamptz`)));
|
|
53
|
+
if (due.length === 0)
|
|
54
|
+
return Ok([]);
|
|
55
|
+
const ids = due.map(d => d.id);
|
|
56
|
+
const updated = [];
|
|
57
|
+
for (const dueId of ids) {
|
|
58
|
+
const rows = await this.db.update(scheduledOrders).set({
|
|
59
|
+
status: "processing",
|
|
60
|
+
processedAt: new Date(),
|
|
61
|
+
updatedAt: new Date(),
|
|
62
|
+
}).where(eq(scheduledOrders.id, dueId)).returning();
|
|
63
|
+
updated.push(rows[0]);
|
|
64
|
+
}
|
|
65
|
+
return Ok(updated);
|
|
66
|
+
}
|
|
67
|
+
async complete(orgId, id) {
|
|
68
|
+
const existing = await this.db.select().from(scheduledOrders)
|
|
69
|
+
.where(and(eq(scheduledOrders.organizationId, orgId), eq(scheduledOrders.id, id)));
|
|
70
|
+
if (existing.length === 0)
|
|
71
|
+
return Err("Scheduled order not found");
|
|
72
|
+
const rows = await this.db.update(scheduledOrders).set({
|
|
73
|
+
status: "completed",
|
|
74
|
+
updatedAt: new Date(),
|
|
75
|
+
}).where(eq(scheduledOrders.id, id)).returning();
|
|
76
|
+
return Ok(rows[0]);
|
|
77
|
+
}
|
|
78
|
+
async expireOld(orgId, hoursThreshold = 24) {
|
|
79
|
+
const cutoff = new Date(Date.now() - hoursThreshold * 60 * 60 * 1000).toISOString();
|
|
80
|
+
const old = await this.db.select().from(scheduledOrders)
|
|
81
|
+
.where(and(eq(scheduledOrders.organizationId, orgId), eq(scheduledOrders.status, "scheduled"), lte(scheduledOrders.scheduledFor, sql `${cutoff}::timestamptz`)));
|
|
82
|
+
if (old.length === 0)
|
|
83
|
+
return Ok([]);
|
|
84
|
+
const updated = [];
|
|
85
|
+
for (const item of old) {
|
|
86
|
+
const rows = await this.db.update(scheduledOrders).set({
|
|
87
|
+
status: "expired",
|
|
88
|
+
updatedAt: new Date(),
|
|
89
|
+
}).where(eq(scheduledOrders.id, item.id)).returning();
|
|
90
|
+
updated.push(rows[0]);
|
|
91
|
+
}
|
|
92
|
+
return Ok(updated);
|
|
93
|
+
}
|
|
94
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { scheduledOrders } from "./schema.js";
|
|
2
|
+
export type { PluginDb as Db } from "@porulle/core";
|
|
3
|
+
export type ScheduledOrder = typeof scheduledOrders.$inferSelect;
|
|
4
|
+
export type ScheduledOrderStatus = "scheduled" | "processing" | "completed" | "cancelled" | "expired";
|
|
5
|
+
export type ScheduledOrderType = "pickup" | "delivery" | "dine_in";
|
|
6
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,YAAY,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,eAAe,CAAC;AACpD,MAAM,MAAM,cAAc,GAAG,OAAO,eAAe,CAAC,YAAY,CAAC;AACjE,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAAG,YAAY,GAAG,WAAW,GAAG,WAAW,GAAG,SAAS,CAAC;AACtG,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@porulle/plugin-scheduled-orders",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"bun": "./src/index.ts",
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"./schema": {
|
|
13
|
+
"bun": "./src/schema.ts",
|
|
14
|
+
"import": "./dist/schema.js",
|
|
15
|
+
"require": "./dist/schema.js",
|
|
16
|
+
"types": "./src/schema.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "rm -rf dist tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
|
|
21
|
+
"check-types": "tsc --noEmit",
|
|
22
|
+
"test": "vitest run"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@hono/zod-openapi": "^1.2.2",
|
|
26
|
+
"@porulle/core": "workspace:*",
|
|
27
|
+
"hono": "^4.12.5"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@repo/eslint-config": "*",
|
|
31
|
+
"@repo/typescript-config": "*",
|
|
32
|
+
"@types/node": "^24.5.2",
|
|
33
|
+
"typescript": "5.9.2",
|
|
34
|
+
"vitest": "^3.2.4"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"src",
|
|
41
|
+
"dist",
|
|
42
|
+
"README.md"
|
|
43
|
+
],
|
|
44
|
+
"description": "Defer order execution: schedule future placement and process due schedules in batch.",
|
|
45
|
+
"homepage": "https://porulle-docs.vercel.app",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/asyncdotengineering/porulle/issues"
|
|
48
|
+
},
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/asyncdotengineering/porulle.git",
|
|
52
|
+
"directory": "packages/plugins/plugin-scheduled-orders"
|
|
53
|
+
},
|
|
54
|
+
"author": "Porulle contributors"
|
|
55
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineCommercePlugin } from "@porulle/core";
|
|
2
|
+
import { scheduledOrders } from "./schema.js";
|
|
3
|
+
import { ScheduledOrderService } from "./services/scheduled-order-service.js";
|
|
4
|
+
import { buildScheduledOrderRoutes } from "./routes/scheduled-orders.js";
|
|
5
|
+
import type { Db } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export type { Db } from "./types.js";
|
|
8
|
+
export { ScheduledOrderService } from "./services/scheduled-order-service.js";
|
|
9
|
+
|
|
10
|
+
export function scheduledOrdersPlugin() {
|
|
11
|
+
return defineCommercePlugin({
|
|
12
|
+
id: "scheduled-orders",
|
|
13
|
+
version: "1.0.0",
|
|
14
|
+
permissions: [
|
|
15
|
+
{ scope: "scheduled-orders:admin", description: "Process due scheduled orders and admin operations." },
|
|
16
|
+
{ scope: "scheduled-orders:create", description: "Create and cancel scheduled orders." },
|
|
17
|
+
{ scope: "scheduled-orders:read", description: "View scheduled orders." },
|
|
18
|
+
],
|
|
19
|
+
schema: () => ({ scheduledOrders }),
|
|
20
|
+
hooks: () => [],
|
|
21
|
+
routes: (ctx) => {
|
|
22
|
+
const db = ctx.database.db;
|
|
23
|
+
if (!db) return [];
|
|
24
|
+
return buildScheduledOrderRoutes(new ScheduledOrderService(db), ctx);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { router } from "@porulle/core";
|
|
2
|
+
import { z } from "@hono/zod-openapi";
|
|
3
|
+
import type { ScheduledOrderService } from "../services/scheduled-order-service.js";
|
|
4
|
+
import type { PluginRouteRegistration } from "@porulle/core";
|
|
5
|
+
import type { ScheduledOrderStatus } from "../types.js";
|
|
6
|
+
|
|
7
|
+
export function buildScheduledOrderRoutes(
|
|
8
|
+
service: ScheduledOrderService,
|
|
9
|
+
ctx: { services?: Record<string, unknown>; database?: { db: unknown } },
|
|
10
|
+
): PluginRouteRegistration[] {
|
|
11
|
+
const r = router("Scheduled Orders", "/scheduled-orders", ctx);
|
|
12
|
+
|
|
13
|
+
r.post("/").summary("Create scheduled order").permission("scheduled-orders:create")
|
|
14
|
+
.input(z.object({
|
|
15
|
+
customerId: z.string().uuid(),
|
|
16
|
+
cartId: z.string().uuid(),
|
|
17
|
+
scheduledFor: z.string().datetime(),
|
|
18
|
+
orderType: z.enum(["pickup", "delivery", "dine_in"]).optional(),
|
|
19
|
+
pickupLocation: z.string().optional(),
|
|
20
|
+
deliveryAddress: z.any().optional(),
|
|
21
|
+
notes: z.string().optional(),
|
|
22
|
+
}))
|
|
23
|
+
.handler(async ({ input, orgId }) => {
|
|
24
|
+
const body = input as {
|
|
25
|
+
customerId: string; cartId: string; scheduledFor: string;
|
|
26
|
+
orderType?: "pickup" | "delivery" | "dine_in";
|
|
27
|
+
pickupLocation?: string; deliveryAddress?: unknown; notes?: string;
|
|
28
|
+
};
|
|
29
|
+
const result = await service.create(orgId, body);
|
|
30
|
+
if (!result.ok) throw new Error(result.error);
|
|
31
|
+
return result.value;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
r.get("/").summary("List scheduled orders").permission("scheduled-orders:read")
|
|
35
|
+
.query(z.object({ status: z.enum(["scheduled", "processing", "completed", "cancelled", "expired"]).optional() }))
|
|
36
|
+
.handler(async ({ query, orgId }) => {
|
|
37
|
+
const q = query as { status?: ScheduledOrderStatus };
|
|
38
|
+
const result = await service.list(orgId, q.status);
|
|
39
|
+
if (!result.ok) throw new Error(result.error);
|
|
40
|
+
return result.value;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
r.get("/{id}").summary("Get scheduled order by ID").permission("scheduled-orders:read")
|
|
44
|
+
.handler(async ({ params, orgId }) => {
|
|
45
|
+
const result = await service.getById(orgId, params.id!);
|
|
46
|
+
if (!result.ok) throw new Error(result.error);
|
|
47
|
+
return result.value;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
r.post("/{id}/cancel").summary("Cancel scheduled order").permission("scheduled-orders:create")
|
|
51
|
+
.handler(async ({ params, orgId }) => {
|
|
52
|
+
const result = await service.cancel(orgId, params.id!);
|
|
53
|
+
if (!result.ok) throw new Error(result.error);
|
|
54
|
+
return result.value;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
r.post("/process-due").summary("Process due scheduled orders").permission("scheduled-orders:admin")
|
|
58
|
+
.input(z.object({ bufferMinutes: z.number().int().min(0).optional() }))
|
|
59
|
+
.handler(async ({ input, orgId }) => {
|
|
60
|
+
const body = input as { bufferMinutes?: number };
|
|
61
|
+
const result = await service.processDue(orgId, body.bufferMinutes);
|
|
62
|
+
if (!result.ok) throw new Error(result.error);
|
|
63
|
+
return result.value;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return r.routes();
|
|
67
|
+
}
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { pgTable, uuid, text, timestamp, index, jsonb } from "@porulle/core/drizzle";
|
|
2
|
+
|
|
3
|
+
export const scheduledOrders = pgTable("scheduled_orders", {
|
|
4
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
5
|
+
organizationId: text("organization_id").notNull(),
|
|
6
|
+
customerId: uuid("customer_id").notNull(),
|
|
7
|
+
cartId: uuid("cart_id").notNull(),
|
|
8
|
+
scheduledFor: timestamp("scheduled_for", { withTimezone: true }).notNull(),
|
|
9
|
+
orderType: text("order_type", { enum: ["pickup", "delivery", "dine_in"] }).notNull().default("pickup"),
|
|
10
|
+
status: text("status", { enum: ["scheduled", "processing", "completed", "cancelled", "expired"] }).notNull().default("scheduled"),
|
|
11
|
+
pickupLocation: text("pickup_location"),
|
|
12
|
+
deliveryAddress: jsonb("delivery_address"),
|
|
13
|
+
notes: text("notes"),
|
|
14
|
+
processedAt: timestamp("processed_at", { withTimezone: true }),
|
|
15
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
16
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
|
17
|
+
}, (table) => ({
|
|
18
|
+
orgIdx: index("idx_scheduled_orders_org").on(table.organizationId),
|
|
19
|
+
statusIdx: index("idx_scheduled_orders_status").on(table.status),
|
|
20
|
+
scheduledForIdx: index("idx_scheduled_orders_scheduled_for").on(table.scheduledFor),
|
|
21
|
+
}));
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { eq, and, lte, sql } from "@porulle/core/drizzle";
|
|
2
|
+
import { Ok, Err } from "@porulle/core";
|
|
3
|
+
import type { PluginResult } from "@porulle/core";
|
|
4
|
+
import { scheduledOrders } from "../schema.js";
|
|
5
|
+
import type { Db, ScheduledOrder, ScheduledOrderStatus, ScheduledOrderType } from "../types.js";
|
|
6
|
+
|
|
7
|
+
export class ScheduledOrderService {
|
|
8
|
+
constructor(private db: Db) {}
|
|
9
|
+
|
|
10
|
+
async create(orgId: string, input: {
|
|
11
|
+
customerId: string;
|
|
12
|
+
cartId: string;
|
|
13
|
+
scheduledFor: string;
|
|
14
|
+
orderType?: ScheduledOrderType;
|
|
15
|
+
pickupLocation?: string;
|
|
16
|
+
deliveryAddress?: unknown;
|
|
17
|
+
notes?: string;
|
|
18
|
+
}): Promise<PluginResult<ScheduledOrder>> {
|
|
19
|
+
const rows = await this.db.insert(scheduledOrders).values({
|
|
20
|
+
organizationId: orgId,
|
|
21
|
+
customerId: input.customerId,
|
|
22
|
+
cartId: input.cartId,
|
|
23
|
+
scheduledFor: new Date(input.scheduledFor),
|
|
24
|
+
orderType: input.orderType ?? "pickup",
|
|
25
|
+
pickupLocation: input.pickupLocation,
|
|
26
|
+
deliveryAddress: input.deliveryAddress,
|
|
27
|
+
notes: input.notes,
|
|
28
|
+
}).returning();
|
|
29
|
+
return Ok(rows[0]!);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async list(orgId: string, status?: ScheduledOrderStatus): Promise<PluginResult<ScheduledOrder[]>> {
|
|
33
|
+
const conditions = [eq(scheduledOrders.organizationId, orgId)];
|
|
34
|
+
if (status) conditions.push(eq(scheduledOrders.status, status));
|
|
35
|
+
const rows = await this.db.select().from(scheduledOrders).where(and(...conditions));
|
|
36
|
+
return Ok(rows);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async getById(orgId: string, id: string): Promise<PluginResult<ScheduledOrder>> {
|
|
40
|
+
const rows = await this.db.select().from(scheduledOrders)
|
|
41
|
+
.where(and(eq(scheduledOrders.organizationId, orgId), eq(scheduledOrders.id, id)));
|
|
42
|
+
if (rows.length === 0) return Err("Scheduled order not found");
|
|
43
|
+
return Ok(rows[0]!);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async cancel(orgId: string, id: string): Promise<PluginResult<ScheduledOrder>> {
|
|
47
|
+
const existing = await this.db.select().from(scheduledOrders)
|
|
48
|
+
.where(and(eq(scheduledOrders.organizationId, orgId), eq(scheduledOrders.id, id)));
|
|
49
|
+
if (existing.length === 0) return Err("Scheduled order not found");
|
|
50
|
+
if (existing[0]!.status !== "scheduled") return Err(`Cannot cancel order with status '${existing[0]!.status}'`);
|
|
51
|
+
const rows = await this.db.update(scheduledOrders).set({
|
|
52
|
+
status: "cancelled",
|
|
53
|
+
updatedAt: new Date(),
|
|
54
|
+
}).where(eq(scheduledOrders.id, id)).returning();
|
|
55
|
+
return Ok(rows[0]!);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async processDue(orgId: string, bufferMinutes: number = 15): Promise<PluginResult<ScheduledOrder[]>> {
|
|
59
|
+
const cutoff = new Date(Date.now() + bufferMinutes * 60 * 1000).toISOString();
|
|
60
|
+
const due = await this.db.select().from(scheduledOrders)
|
|
61
|
+
.where(and(
|
|
62
|
+
eq(scheduledOrders.organizationId, orgId),
|
|
63
|
+
eq(scheduledOrders.status, "scheduled"),
|
|
64
|
+
lte(scheduledOrders.scheduledFor, sql`${cutoff}::timestamptz`),
|
|
65
|
+
));
|
|
66
|
+
if (due.length === 0) return Ok([]);
|
|
67
|
+
const ids = due.map(d => d.id);
|
|
68
|
+
const updated: ScheduledOrder[] = [];
|
|
69
|
+
for (const dueId of ids) {
|
|
70
|
+
const rows = await this.db.update(scheduledOrders).set({
|
|
71
|
+
status: "processing",
|
|
72
|
+
processedAt: new Date(),
|
|
73
|
+
updatedAt: new Date(),
|
|
74
|
+
}).where(eq(scheduledOrders.id, dueId)).returning();
|
|
75
|
+
updated.push(rows[0]!);
|
|
76
|
+
}
|
|
77
|
+
return Ok(updated);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async complete(orgId: string, id: string): Promise<PluginResult<ScheduledOrder>> {
|
|
81
|
+
const existing = await this.db.select().from(scheduledOrders)
|
|
82
|
+
.where(and(eq(scheduledOrders.organizationId, orgId), eq(scheduledOrders.id, id)));
|
|
83
|
+
if (existing.length === 0) return Err("Scheduled order not found");
|
|
84
|
+
const rows = await this.db.update(scheduledOrders).set({
|
|
85
|
+
status: "completed",
|
|
86
|
+
updatedAt: new Date(),
|
|
87
|
+
}).where(eq(scheduledOrders.id, id)).returning();
|
|
88
|
+
return Ok(rows[0]!);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async expireOld(orgId: string, hoursThreshold: number = 24): Promise<PluginResult<ScheduledOrder[]>> {
|
|
92
|
+
const cutoff = new Date(Date.now() - hoursThreshold * 60 * 60 * 1000).toISOString();
|
|
93
|
+
const old = await this.db.select().from(scheduledOrders)
|
|
94
|
+
.where(and(
|
|
95
|
+
eq(scheduledOrders.organizationId, orgId),
|
|
96
|
+
eq(scheduledOrders.status, "scheduled"),
|
|
97
|
+
lte(scheduledOrders.scheduledFor, sql`${cutoff}::timestamptz`),
|
|
98
|
+
));
|
|
99
|
+
if (old.length === 0) return Ok([]);
|
|
100
|
+
const updated: ScheduledOrder[] = [];
|
|
101
|
+
for (const item of old) {
|
|
102
|
+
const rows = await this.db.update(scheduledOrders).set({
|
|
103
|
+
status: "expired",
|
|
104
|
+
updatedAt: new Date(),
|
|
105
|
+
}).where(eq(scheduledOrders.id, item.id)).returning();
|
|
106
|
+
updated.push(rows[0]!);
|
|
107
|
+
}
|
|
108
|
+
return Ok(updated);
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { scheduledOrders } from "./schema.js";
|
|
2
|
+
|
|
3
|
+
export type { PluginDb as Db } from "@porulle/core";
|
|
4
|
+
export type ScheduledOrder = typeof scheduledOrders.$inferSelect;
|
|
5
|
+
export type ScheduledOrderStatus = "scheduled" | "processing" | "completed" | "cancelled" | "expired";
|
|
6
|
+
export type ScheduledOrderType = "pickup" | "delivery" | "dine_in";
|