@unifiedcommerce/plugin-notifications 0.0.1

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 (38) hide show
  1. package/dist/adapters/console.d.ts +17 -0
  2. package/dist/adapters/console.d.ts.map +1 -0
  3. package/dist/adapters/console.js +44 -0
  4. package/dist/adapters/types.d.ts +62 -0
  5. package/dist/adapters/types.d.ts.map +1 -0
  6. package/dist/adapters/types.js +1 -0
  7. package/dist/index.d.ts +24 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +46 -0
  10. package/dist/routes/notifications.d.ts +11 -0
  11. package/dist/routes/notifications.d.ts.map +1 -0
  12. package/dist/routes/notifications.js +170 -0
  13. package/dist/schema.d.ts +624 -0
  14. package/dist/schema.d.ts.map +1 -0
  15. package/dist/schema.js +59 -0
  16. package/dist/services/notification-service.d.ts +52 -0
  17. package/dist/services/notification-service.d.ts.map +1 -0
  18. package/dist/services/notification-service.js +220 -0
  19. package/dist/services/preference-service.d.ts +23 -0
  20. package/dist/services/preference-service.d.ts.map +1 -0
  21. package/dist/services/preference-service.js +69 -0
  22. package/dist/services/print-service.d.ts +32 -0
  23. package/dist/services/print-service.d.ts.map +1 -0
  24. package/dist/services/print-service.js +91 -0
  25. package/dist/tsconfig.tsbuildinfo +1 -0
  26. package/dist/types.d.ts +15 -0
  27. package/dist/types.d.ts.map +1 -0
  28. package/dist/types.js +2 -0
  29. package/package.json +37 -0
  30. package/src/adapters/console.ts +52 -0
  31. package/src/adapters/types.ts +56 -0
  32. package/src/index.ts +53 -0
  33. package/src/routes/notifications.ts +199 -0
  34. package/src/schema.ts +67 -0
  35. package/src/services/notification-service.ts +270 -0
  36. package/src/services/preference-service.ts +92 -0
  37. package/src/services/print-service.ts +99 -0
  38. package/src/types.ts +16 -0
@@ -0,0 +1,15 @@
1
+ import type { notificationTemplates, customerNotificationPrefs, notificationLog, printJobs } from "./schema";
2
+ export type { PluginDb as Db } from "@unifiedcommerce/core";
3
+ export type NotificationTemplate = typeof notificationTemplates.$inferSelect;
4
+ export type CustomerNotificationPref = typeof customerNotificationPrefs.$inferSelect;
5
+ export type NotificationLogEntry = typeof notificationLog.$inferSelect;
6
+ export type PrintJob = typeof printJobs.$inferSelect;
7
+ export type Channel = "email" | "sms" | "push" | "print";
8
+ export type PrefChannel = "email" | "sms" | "push";
9
+ export type NotificationStatus = "queued" | "sent" | "delivered" | "failed";
10
+ export type PrintJobStatus = "queued" | "printing" | "printed" | "failed";
11
+ export type PrintJobType = "receipt" | "label" | "sticker" | "kot";
12
+ /** Result type re-exports from core. */
13
+ export { Ok, Err } from "@unifiedcommerce/core";
14
+ export type { PluginResult as Result, PluginResultErr as ResultErr } from "@unifiedcommerce/core";
15
+ //# 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,qBAAqB,EAAE,yBAAyB,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE7G,YAAY,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,uBAAuB,CAAC;AAC5D,MAAM,MAAM,oBAAoB,GAAG,OAAO,qBAAqB,CAAC,YAAY,CAAC;AAC7E,MAAM,MAAM,wBAAwB,GAAG,OAAO,yBAAyB,CAAC,YAAY,CAAC;AACrF,MAAM,MAAM,oBAAoB,GAAG,OAAO,eAAe,CAAC,YAAY,CAAC;AACvE,MAAM,MAAM,QAAQ,GAAG,OAAO,SAAS,CAAC,YAAY,CAAC;AACrD,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;AACzD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AACnD,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;AAC5E,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;AAC1E,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,KAAK,CAAC;AAEnE,wCAAwC;AACxC,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,uBAAuB,CAAC;AAChD,YAAY,EAAE,YAAY,IAAI,MAAM,EAAE,eAAe,IAAI,SAAS,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ /** Result type re-exports from core. */
2
+ export { Ok, Err } from "@unifiedcommerce/core";
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@unifiedcommerce/plugin-notifications",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "default": "./dist/index.js"
9
+ }
10
+ },
11
+ "scripts": {
12
+ "build": "tsc -p tsconfig.build.json",
13
+ "check-types": "tsc --noEmit",
14
+ "test": "vitest run"
15
+ },
16
+ "dependencies": {
17
+ "@hono/zod-openapi": "^1.2.2",
18
+ "@unifiedcommerce/core": "*",
19
+ "drizzle-orm": "^0.45.1",
20
+ "hono": "^4.12.5"
21
+ },
22
+ "devDependencies": {
23
+ "@repo/eslint-config": "*",
24
+ "@repo/typescript-config": "*",
25
+ "@types/node": "^24.5.2",
26
+ "typescript": "5.9.2",
27
+ "vitest": "^3.2.4"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "src",
35
+ "README.md"
36
+ ]
37
+ }
@@ -0,0 +1,52 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Ok } from "../types";
3
+ import type { SMSAdapter, PushAdapter, PrintAdapter } from "./types";
4
+
5
+ /**
6
+ * Console SMS adapter — logs SMS messages to stdout.
7
+ * Use for development and testing. Replace with a real adapter (e.g. Twilio) in production.
8
+ */
9
+ export function consoleSMSAdapter(): SMSAdapter {
10
+ return {
11
+ providerId: "console",
12
+ async send(params) {
13
+ const messageId = `sms_${randomUUID()}`;
14
+ console.log(`[SMS:console] to=${params.to} body="${params.body}" messageId=${messageId}`);
15
+ return Ok({ messageId });
16
+ },
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Console Push adapter — logs push notifications to stdout.
22
+ * Use for development and testing. Replace with a real adapter (e.g. FCM) in production.
23
+ */
24
+ export function consolePushAdapter(): PushAdapter {
25
+ return {
26
+ providerId: "console",
27
+ async send(params) {
28
+ const messageId = `push_${randomUUID()}`;
29
+ console.log(
30
+ `[Push:console] token=${params.deviceToken} title="${params.title}" body="${params.body}" messageId=${messageId}`,
31
+ );
32
+ return Ok({ messageId });
33
+ },
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Console Print adapter — logs print jobs to stdout.
39
+ * Use for development and testing. Replace with a real adapter (e.g. ESC/POS) in production.
40
+ */
41
+ export function consolePrintAdapter(): PrintAdapter {
42
+ return {
43
+ providerId: "console",
44
+ async print(params) {
45
+ const jobId = `print_${randomUUID()}`;
46
+ console.log(
47
+ `[Print:console] printer=${params.printerId} format=${params.format} jobId=${jobId}`,
48
+ );
49
+ return Ok({ jobId });
50
+ },
51
+ };
52
+ }
@@ -0,0 +1,56 @@
1
+ import type { Result } from "../types";
2
+
3
+ /**
4
+ * SMS Adapter interface.
5
+ *
6
+ * Implement this interface to integrate with an SMS provider (e.g. Twilio, Vonage).
7
+ * The `send` method delivers a text message to the given phone number.
8
+ */
9
+ export interface SMSAdapter {
10
+ /** Unique provider identifier (e.g. "twilio", "vonage", "console"). */
11
+ providerId: string;
12
+ /** Send an SMS message to the given phone number. */
13
+ send(params: { to: string; body: string }): Promise<Result<{ messageId: string }>>;
14
+ }
15
+
16
+ /**
17
+ * Push Notification Adapter interface.
18
+ *
19
+ * Implement this interface to integrate with a push notification service
20
+ * (e.g. Firebase Cloud Messaging, Apple Push Notification Service).
21
+ */
22
+ export interface PushAdapter {
23
+ /** Unique provider identifier (e.g. "fcm", "apns", "console"). */
24
+ providerId: string;
25
+ /** Send a push notification to the given device token. */
26
+ send(params: {
27
+ deviceToken: string;
28
+ title: string;
29
+ body: string;
30
+ data?: Record<string, unknown>;
31
+ }): Promise<Result<{ messageId: string }>>;
32
+ }
33
+
34
+ /**
35
+ * Print Adapter interface.
36
+ *
37
+ * Implement this interface to integrate with a receipt/label printer
38
+ * (e.g. Epson ESC/POS, Star Line Mode printers).
39
+ */
40
+ export interface PrintAdapter {
41
+ /** Unique provider identifier (e.g. "esc_pos", "star", "console"). */
42
+ providerId: string;
43
+ /** Send a print job to the given printer. */
44
+ print(params: {
45
+ printerId: string;
46
+ content: Record<string, unknown>;
47
+ format: "esc_pos" | "star_line" | "label";
48
+ }): Promise<Result<{ jobId: string }>>;
49
+ }
50
+
51
+ /** Configuration object for notification adapters. */
52
+ export interface NotificationAdapters {
53
+ sms?: SMSAdapter;
54
+ push?: PushAdapter;
55
+ print?: PrintAdapter;
56
+ }
package/src/index.ts ADDED
@@ -0,0 +1,53 @@
1
+ import { defineCommercePlugin } from "@unifiedcommerce/core";
2
+ import { notificationTemplates, customerNotificationPrefs, notificationLog, printJobs } from "./schema";
3
+ import { NotificationService } from "./services/notification-service";
4
+ import { PreferenceService } from "./services/preference-service";
5
+ import { PrintService } from "./services/print-service";
6
+ import { buildNotificationRoutes } from "./routes/notifications";
7
+ import type { Db } from "./types";
8
+ import type { NotificationAdapters } from "./adapters/types";
9
+
10
+ // Re-exports for consumers
11
+ export type { Db } from "./types";
12
+ export type { NotificationTemplate, CustomerNotificationPref, NotificationLogEntry, PrintJob, Result, ResultErr, Channel, PrefChannel, NotificationStatus, PrintJobStatus, PrintJobType } from "./types";
13
+ export type { SMSAdapter, PushAdapter, PrintAdapter, NotificationAdapters } from "./adapters/types";
14
+ export { NotificationService } from "./services/notification-service";
15
+ export { PreferenceService } from "./services/preference-service";
16
+ export { PrintService } from "./services/print-service";
17
+ export { consoleSMSAdapter, consolePushAdapter, consolePrintAdapter } from "./adapters/console";
18
+
19
+ /**
20
+ * Notifications Plugin (RFC-030)
21
+ *
22
+ * Provides:
23
+ * - Notification template CRUD with Handlebars-style rendering
24
+ * - Multi-channel dispatch (email, SMS, push, print)
25
+ * - Customer notification preferences (opt-out model)
26
+ * - Notification log with status tracking
27
+ * - Print job management (receipt, label, sticker, KOT)
28
+ * - Pluggable adapter architecture for SMS, Push, and Print providers
29
+ *
30
+ * @param adapters - Optional adapter configuration. Pass console adapters for dev,
31
+ * or real adapters (Twilio, FCM, ESC/POS) for production.
32
+ */
33
+ export function notificationsPlugin(adapters?: NotificationAdapters) {
34
+ return defineCommercePlugin({
35
+ id: "notifications",
36
+ version: "1.0.0",
37
+ permissions: [
38
+ { scope: "notifications:admin", description: "Manage notification templates, send notifications, view log, manage print jobs." },
39
+ { scope: "notifications:write", description: "Set customer notification preferences." },
40
+ { scope: "notifications:read", description: "View customer notification preferences." },
41
+ ],
42
+ schema: () => ({ notificationTemplates, customerNotificationPrefs, notificationLog, printJobs }),
43
+ hooks: () => [],
44
+ routes: (ctx) => {
45
+ const db = ctx.database.db as unknown as Db;
46
+ if (!db) return [];
47
+ const notifService = new NotificationService(db, adapters);
48
+ const prefService = new PreferenceService(db);
49
+ const printService = new PrintService(db, adapters?.print);
50
+ return buildNotificationRoutes(notifService, prefService, printService, ctx);
51
+ },
52
+ });
53
+ }
@@ -0,0 +1,199 @@
1
+ import { router } from "@unifiedcommerce/core";
2
+ import { z } from "@hono/zod-openapi";
3
+ import type { NotificationService } from "../services/notification-service";
4
+ import type { PreferenceService } from "../services/preference-service";
5
+ import type { PrintService } from "../services/print-service";
6
+ import type { PluginRouteRegistration } from "@unifiedcommerce/core";
7
+
8
+ export function buildNotificationRoutes(
9
+ notifService: NotificationService,
10
+ prefService: PreferenceService,
11
+ printService: PrintService,
12
+ ctx: { services?: Record<string, unknown>; database?: { db: unknown } },
13
+ ): PluginRouteRegistration[] {
14
+ // ── Template Routes ────────────────────────────────────────────────
15
+ const tmpl = router("Notification Templates", "/notifications/templates", ctx);
16
+
17
+ tmpl.post("/").summary("Create notification template").permission("notifications:admin")
18
+ .input(z.object({
19
+ event: z.string().min(1),
20
+ channel: z.enum(["email", "sms", "push", "print"]),
21
+ subject: z.string().optional(),
22
+ bodyTemplate: z.string().min(1),
23
+ }))
24
+ .handler(async ({ input, orgId }) => {
25
+ const body = input as { event: string; channel: "email" | "sms" | "push" | "print"; subject?: string; bodyTemplate: string };
26
+ const result = await notifService.createTemplate(orgId, body);
27
+ if (!result.ok) throw new Error(result.error);
28
+ return result.value;
29
+ });
30
+
31
+ tmpl.get("/").summary("List notification templates").permission("notifications:admin")
32
+ .query(z.object({
33
+ event: z.string().optional(),
34
+ channel: z.enum(["email", "sms", "push", "print"]).optional(),
35
+ }))
36
+ .handler(async ({ query, orgId }) => {
37
+ const q = query as { event?: string; channel?: "email" | "sms" | "push" | "print" };
38
+ const result = await notifService.listTemplates(orgId, q);
39
+ if (!result.ok) throw new Error(result.error);
40
+ return result.value;
41
+ });
42
+
43
+ tmpl.get("/{id}").summary("Get notification template").permission("notifications:admin")
44
+ .handler(async ({ params, orgId }) => {
45
+ const result = await notifService.getTemplate(orgId, params.id!);
46
+ if (!result.ok) throw new Error(result.error);
47
+ return result.value;
48
+ });
49
+
50
+ tmpl.patch("/{id}").summary("Update notification template").permission("notifications:admin")
51
+ .input(z.object({
52
+ subject: z.string().optional(),
53
+ bodyTemplate: z.string().optional(),
54
+ isActive: z.boolean().optional(),
55
+ }))
56
+ .handler(async ({ params, input, orgId }) => {
57
+ const body = input as { subject?: string; bodyTemplate?: string; isActive?: boolean };
58
+ const result = await notifService.updateTemplate(orgId, params.id!, body);
59
+ if (!result.ok) throw new Error(result.error);
60
+ return result.value;
61
+ });
62
+
63
+ tmpl.delete("/{id}").summary("Soft-delete notification template").permission("notifications:admin")
64
+ .handler(async ({ params, orgId }) => {
65
+ const result = await notifService.deleteTemplate(orgId, params.id!);
66
+ if (!result.ok) throw new Error(result.error);
67
+ return result.value;
68
+ });
69
+
70
+ // ── Send Route ─────────────────────────────────────────────────────
71
+ const send = router("Notifications", "/notifications", ctx);
72
+
73
+ send.post("/send").summary("Send notification").permission("notifications:admin")
74
+ .input(z.object({
75
+ event: z.string().min(1),
76
+ recipient: z.string().min(1),
77
+ channel: z.enum(["email", "sms", "push", "print"]),
78
+ customerId: z.string().uuid().optional(),
79
+ data: z.record(z.string(), z.unknown()).optional(),
80
+ metadata: z.record(z.string(), z.unknown()).optional(),
81
+ }))
82
+ .handler(async ({ input, orgId }) => {
83
+ const body = input as {
84
+ event: string; recipient: string; channel: "email" | "sms" | "push" | "print";
85
+ customerId?: string; data?: Record<string, unknown>; metadata?: Record<string, unknown>;
86
+ };
87
+ const result = await notifService.send(orgId, body);
88
+ if (!result.ok) throw new Error(result.error);
89
+ return result.value;
90
+ });
91
+
92
+ // ── Log Route ──────────────────────────────────────────────────────
93
+ send.get("/log").summary("Query notification log").permission("notifications:admin")
94
+ .query(z.object({
95
+ channel: z.string().optional(),
96
+ event: z.string().optional(),
97
+ status: z.enum(["queued", "sent", "delivered", "failed"]).optional(),
98
+ limit: z.coerce.number().int().positive().optional(),
99
+ }))
100
+ .handler(async ({ query, orgId }) => {
101
+ const q = query as { channel?: string; event?: string; status?: "queued" | "sent" | "delivered" | "failed"; limit?: number };
102
+ const result = await notifService.listLog(orgId, q);
103
+ if (!result.ok) throw new Error(result.error);
104
+ return result.value;
105
+ });
106
+
107
+ // ── Preference Routes ──────────────────────────────────────────────
108
+ const pref = router("Notification Preferences", "/notifications/preferences", ctx);
109
+
110
+ pref.post("/").summary("Set customer notification preference").permission("notifications:write")
111
+ .input(z.object({
112
+ customerId: z.string().uuid(),
113
+ channel: z.enum(["email", "sms", "push"]),
114
+ isEnabled: z.boolean(),
115
+ destination: z.string().optional(),
116
+ }))
117
+ .handler(async ({ input, orgId }) => {
118
+ const body = input as { customerId: string; channel: "email" | "sms" | "push"; isEnabled: boolean; destination?: string };
119
+ const result = await prefService.setPreference(
120
+ orgId, body.customerId, body.channel, body.isEnabled, body.destination,
121
+ );
122
+ if (!result.ok) throw new Error(result.error);
123
+ return result.value;
124
+ });
125
+
126
+ pref.get("/{customerId}").summary("Get customer notification preferences").permission("notifications:read")
127
+ .handler(async ({ params, orgId }) => {
128
+ const result = await prefService.getPreferences(orgId, params.customerId!);
129
+ if (!result.ok) throw new Error(result.error);
130
+ return result.value;
131
+ });
132
+
133
+ // ── Print Routes ───────────────────────────────────────────────────
134
+ const print = router("Print Jobs", "/notifications/print", ctx);
135
+
136
+ print.post("/").summary("Submit print job").permission("notifications:admin")
137
+ .input(z.object({
138
+ type: z.enum(["receipt", "label", "sticker", "kot"]),
139
+ printerId: z.string().min(1),
140
+ content: z.record(z.string(), z.unknown()),
141
+ format: z.enum(["esc_pos", "star_line", "label"]).optional(),
142
+ }))
143
+ .handler(async ({ input, orgId }) => {
144
+ const body = input as {
145
+ type: "receipt" | "label" | "sticker" | "kot";
146
+ printerId: string;
147
+ content: Record<string, unknown>;
148
+ format?: "esc_pos" | "star_line" | "label";
149
+ };
150
+ const result = await printService.submitJob(orgId, body);
151
+ if (!result.ok) throw new Error(result.error);
152
+ return result.value;
153
+ });
154
+
155
+ print.get("/{id}").summary("Get print job").permission("notifications:admin")
156
+ .handler(async ({ params, orgId }) => {
157
+ const result = await printService.getJob(orgId, params.id!);
158
+ if (!result.ok) throw new Error(result.error);
159
+ return result.value;
160
+ });
161
+
162
+ print.get("/").summary("List print jobs").permission("notifications:admin")
163
+ .query(z.object({
164
+ status: z.enum(["queued", "printing", "printed", "failed"]).optional(),
165
+ printerId: z.string().optional(),
166
+ type: z.enum(["receipt", "label", "sticker", "kot"]).optional(),
167
+ limit: z.coerce.number().int().positive().optional(),
168
+ }))
169
+ .handler(async ({ query, orgId }) => {
170
+ const q = query as {
171
+ status?: "queued" | "printing" | "printed" | "failed";
172
+ printerId?: string;
173
+ type?: "receipt" | "label" | "sticker" | "kot";
174
+ limit?: number;
175
+ };
176
+ const result = await printService.listJobs(orgId, q);
177
+ if (!result.ok) throw new Error(result.error);
178
+ return result.value;
179
+ });
180
+
181
+ print.patch("/{id}/status").summary("Update print job status").permission("notifications:admin")
182
+ .input(z.object({
183
+ status: z.enum(["queued", "printing", "printed", "failed"]),
184
+ error: z.string().optional(),
185
+ }))
186
+ .handler(async ({ params, input, orgId }) => {
187
+ const body = input as { status: "queued" | "printing" | "printed" | "failed"; error?: string };
188
+ const result = await printService.updateJobStatus(orgId, params.id!, body.status, body.error);
189
+ if (!result.ok) throw new Error(result.error);
190
+ return result.value;
191
+ });
192
+
193
+ return [
194
+ ...tmpl.routes(),
195
+ ...send.routes(),
196
+ ...pref.routes(),
197
+ ...print.routes(),
198
+ ];
199
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { pgTable, uuid, text, boolean, timestamp, index, uniqueIndex, jsonb } from "drizzle-orm/pg-core";
2
+
3
+ export const notificationTemplates = pgTable("notification_templates", {
4
+ id: uuid("id").defaultRandom().primaryKey(),
5
+ organizationId: text("organization_id").notNull(),
6
+ event: text("event").notNull(),
7
+ channel: text("channel", { enum: ["email", "sms", "push", "print"] }).notNull(),
8
+ subject: text("subject"),
9
+ bodyTemplate: text("body_template").notNull(),
10
+ isActive: boolean("is_active").notNull().default(true),
11
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
12
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
13
+ }, (table) => ({
14
+ orgIdx: index("idx_notification_templates_org").on(table.organizationId),
15
+ orgEventChannelUnique: uniqueIndex("notification_templates_org_event_channel_unique").on(
16
+ table.organizationId, table.event, table.channel,
17
+ ),
18
+ }));
19
+
20
+ export const customerNotificationPrefs = pgTable("customer_notification_prefs", {
21
+ id: uuid("id").defaultRandom().primaryKey(),
22
+ organizationId: text("organization_id").notNull(),
23
+ customerId: uuid("customer_id").notNull(),
24
+ channel: text("channel", { enum: ["email", "sms", "push"] }).notNull(),
25
+ isEnabled: boolean("is_enabled").notNull().default(true),
26
+ destination: text("destination"),
27
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
28
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
29
+ }, (table) => ({
30
+ orgIdx: index("idx_customer_notification_prefs_org").on(table.organizationId),
31
+ orgCustomerChannelUnique: uniqueIndex("customer_notification_prefs_org_cust_channel_unique").on(
32
+ table.organizationId, table.customerId, table.channel,
33
+ ),
34
+ }));
35
+
36
+ export const notificationLog = pgTable("notification_log", {
37
+ id: uuid("id").defaultRandom().primaryKey(),
38
+ organizationId: text("organization_id").notNull(),
39
+ channel: text("channel").notNull(),
40
+ event: text("event").notNull(),
41
+ recipient: text("recipient").notNull(),
42
+ status: text("status", { enum: ["queued", "sent", "delivered", "failed"] }).notNull().default("queued"),
43
+ error: text("error"),
44
+ metadata: jsonb("metadata").notNull().default({}),
45
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
46
+ }, (table) => ({
47
+ orgIdx: index("idx_notification_log_org").on(table.organizationId),
48
+ channelIdx: index("idx_notification_log_channel").on(table.channel),
49
+ eventIdx: index("idx_notification_log_event").on(table.event),
50
+ statusIdx: index("idx_notification_log_status").on(table.status),
51
+ }));
52
+
53
+ export const printJobs = pgTable("print_jobs", {
54
+ id: uuid("id").defaultRandom().primaryKey(),
55
+ organizationId: text("organization_id").notNull(),
56
+ type: text("type", { enum: ["receipt", "label", "sticker", "kot"] }).notNull(),
57
+ printerId: text("printer_id").notNull(),
58
+ content: jsonb("content").notNull().default({}),
59
+ status: text("status", { enum: ["queued", "printing", "printed", "failed"] }).notNull().default("queued"),
60
+ error: text("error"),
61
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
62
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
63
+ }, (table) => ({
64
+ orgIdx: index("idx_print_jobs_org").on(table.organizationId),
65
+ statusIdx: index("idx_print_jobs_status").on(table.status),
66
+ printerIdx: index("idx_print_jobs_printer").on(table.printerId),
67
+ }));