@unifiedcommerce/plugin-reviews 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.
@@ -0,0 +1,4 @@
1
+ export type { Db } from "./types";
2
+ export { ReviewService } from "./services/review-service";
3
+ export declare function reviewsPlugin(): import("@unifiedcommerce/core").CommercePlugin;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,YAAY,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE1D,wBAAgB,aAAa,mDAiB5B"}
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ import { defineCommercePlugin } from "@unifiedcommerce/core";
2
+ import { customerReviews } from "./schema";
3
+ import { ReviewService } from "./services/review-service";
4
+ import { buildReviewRoutes } from "./routes/reviews";
5
+ export { ReviewService } from "./services/review-service";
6
+ export function reviewsPlugin() {
7
+ return defineCommercePlugin({
8
+ id: "reviews",
9
+ version: "1.0.0",
10
+ permissions: [
11
+ { scope: "reviews:admin", description: "Approve, reject, and reply to reviews." },
12
+ { scope: "reviews:write", description: "Submit reviews." },
13
+ { scope: "reviews:read", description: "View reviews and summaries." },
14
+ ],
15
+ schema: () => ({ customerReviews }),
16
+ hooks: () => [],
17
+ routes: (ctx) => {
18
+ const db = ctx.database.db;
19
+ if (!db)
20
+ return [];
21
+ return buildReviewRoutes(new ReviewService(db), ctx);
22
+ },
23
+ });
24
+ }
@@ -0,0 +1,9 @@
1
+ import type { ReviewService } from "../services/review-service";
2
+ import type { PluginRouteRegistration } from "@unifiedcommerce/core";
3
+ export declare function buildReviewRoutes(service: ReviewService, ctx: {
4
+ services?: Record<string, unknown>;
5
+ database?: {
6
+ db: unknown;
7
+ };
8
+ }): PluginRouteRegistration[];
9
+ //# sourceMappingURL=reviews.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reviews.d.ts","sourceRoot":"","sources":["../../src/routes/reviews.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAErE,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,aAAa,EACtB,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,CAwE3B"}
@@ -0,0 +1,72 @@
1
+ import { router } from "@unifiedcommerce/core";
2
+ import { z } from "@hono/zod-openapi";
3
+ export function buildReviewRoutes(service, ctx) {
4
+ const r = router("Reviews", "/reviews", ctx);
5
+ r.post("/").summary("Submit review").permission("reviews:write")
6
+ .input(z.object({
7
+ customerId: z.string().uuid().optional(),
8
+ entityId: z.string().uuid(),
9
+ orderId: z.string().uuid().optional(),
10
+ rating: z.number().int().min(1).max(5),
11
+ title: z.string().optional(),
12
+ body: z.string().optional(),
13
+ }))
14
+ .handler(async ({ input, orgId }) => {
15
+ const body = input;
16
+ const result = await service.submit(orgId, body);
17
+ if (!result.ok)
18
+ throw new Error(result.error);
19
+ return result.value;
20
+ });
21
+ r.get("/entity/{entityId}").summary("List reviews for entity").permission("reviews:read")
22
+ .handler(async ({ params, orgId }) => {
23
+ const result = await service.listForEntity(orgId, params.entityId);
24
+ if (!result.ok)
25
+ throw new Error(result.error);
26
+ return result.value;
27
+ });
28
+ r.get("/entity/{entityId}/summary").summary("Review summary for entity").permission("reviews:read")
29
+ .handler(async ({ params, orgId }) => {
30
+ const result = await service.getSummary(orgId, params.entityId);
31
+ if (!result.ok)
32
+ throw new Error(result.error);
33
+ return result.value;
34
+ });
35
+ r.patch("/{id}/approve").summary("Approve review").permission("reviews:admin")
36
+ .handler(async ({ params, orgId }) => {
37
+ const result = await service.approve(orgId, params.id);
38
+ if (!result.ok)
39
+ throw new Error(result.error);
40
+ return result.value;
41
+ });
42
+ r.patch("/{id}/reject").summary("Reject review").permission("reviews:admin")
43
+ .handler(async ({ params, orgId }) => {
44
+ const result = await service.reject(orgId, params.id);
45
+ if (!result.ok)
46
+ throw new Error(result.error);
47
+ return result.value;
48
+ });
49
+ r.post("/{id}/reply").summary("Reply to review").permission("reviews:admin")
50
+ .input(z.object({
51
+ response: z.string().min(1),
52
+ responseBy: z.string().min(1),
53
+ }))
54
+ .handler(async ({ params, input, orgId }) => {
55
+ const body = input;
56
+ const result = await service.reply(orgId, params.id, body.response, body.responseBy);
57
+ if (!result.ok)
58
+ throw new Error(result.error);
59
+ return result.value;
60
+ });
61
+ r.get("/mine").summary("My reviews").permission("reviews:write")
62
+ .handler(async ({ actor, orgId }) => {
63
+ // Use the authenticated actor's userId — never accept customerId from query
64
+ if (!actor?.userId)
65
+ throw new Error("Authentication required");
66
+ const result = await service.listByCustomer(orgId, actor.userId);
67
+ if (!result.ok)
68
+ throw new Error(result.error);
69
+ return result.value;
70
+ });
71
+ return r.routes();
72
+ }
@@ -0,0 +1,280 @@
1
+ export declare const customerReviews: import("drizzle-orm/pg-core").PgTableWithColumns<{
2
+ name: "customer_reviews";
3
+ schema: undefined;
4
+ columns: {
5
+ id: import("drizzle-orm/pg-core").PgColumn<{
6
+ name: "id";
7
+ tableName: "customer_reviews";
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("drizzle-orm/pg-core").PgColumn<{
23
+ name: "organization_id";
24
+ tableName: "customer_reviews";
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("drizzle-orm/pg-core").PgColumn<{
40
+ name: "customer_id";
41
+ tableName: "customer_reviews";
42
+ dataType: "string";
43
+ columnType: "PgUUID";
44
+ data: string;
45
+ driverParam: string;
46
+ notNull: false;
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
+ entityId: import("drizzle-orm/pg-core").PgColumn<{
57
+ name: "entity_id";
58
+ tableName: "customer_reviews";
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
+ orderId: import("drizzle-orm/pg-core").PgColumn<{
74
+ name: "order_id";
75
+ tableName: "customer_reviews";
76
+ dataType: "string";
77
+ columnType: "PgUUID";
78
+ data: string;
79
+ driverParam: string;
80
+ notNull: false;
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
+ rating: import("drizzle-orm/pg-core").PgColumn<{
91
+ name: "rating";
92
+ tableName: "customer_reviews";
93
+ dataType: "number";
94
+ columnType: "PgInteger";
95
+ data: number;
96
+ driverParam: string | number;
97
+ notNull: true;
98
+ hasDefault: false;
99
+ isPrimaryKey: false;
100
+ isAutoincrement: false;
101
+ hasRuntimeDefault: false;
102
+ enumValues: undefined;
103
+ baseColumn: never;
104
+ identity: undefined;
105
+ generated: undefined;
106
+ }, {}, {}>;
107
+ title: import("drizzle-orm/pg-core").PgColumn<{
108
+ name: "title";
109
+ tableName: "customer_reviews";
110
+ dataType: "string";
111
+ columnType: "PgText";
112
+ data: string;
113
+ driverParam: string;
114
+ notNull: false;
115
+ hasDefault: false;
116
+ isPrimaryKey: false;
117
+ isAutoincrement: false;
118
+ hasRuntimeDefault: false;
119
+ enumValues: [string, ...string[]];
120
+ baseColumn: never;
121
+ identity: undefined;
122
+ generated: undefined;
123
+ }, {}, {}>;
124
+ body: import("drizzle-orm/pg-core").PgColumn<{
125
+ name: "body";
126
+ tableName: "customer_reviews";
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
+ status: import("drizzle-orm/pg-core").PgColumn<{
142
+ name: "status";
143
+ tableName: "customer_reviews";
144
+ dataType: "string";
145
+ columnType: "PgText";
146
+ data: "pending" | "approved" | "rejected";
147
+ driverParam: string;
148
+ notNull: true;
149
+ hasDefault: true;
150
+ isPrimaryKey: false;
151
+ isAutoincrement: false;
152
+ hasRuntimeDefault: false;
153
+ enumValues: ["pending", "approved", "rejected"];
154
+ baseColumn: never;
155
+ identity: undefined;
156
+ generated: undefined;
157
+ }, {}, {}>;
158
+ isVerified: import("drizzle-orm/pg-core").PgColumn<{
159
+ name: "is_verified";
160
+ tableName: "customer_reviews";
161
+ dataType: "boolean";
162
+ columnType: "PgBoolean";
163
+ data: boolean;
164
+ driverParam: boolean;
165
+ notNull: true;
166
+ hasDefault: true;
167
+ isPrimaryKey: false;
168
+ isAutoincrement: false;
169
+ hasRuntimeDefault: false;
170
+ enumValues: undefined;
171
+ baseColumn: never;
172
+ identity: undefined;
173
+ generated: undefined;
174
+ }, {}, {}>;
175
+ isPublished: import("drizzle-orm/pg-core").PgColumn<{
176
+ name: "is_published";
177
+ tableName: "customer_reviews";
178
+ dataType: "boolean";
179
+ columnType: "PgBoolean";
180
+ data: boolean;
181
+ driverParam: boolean;
182
+ notNull: true;
183
+ hasDefault: true;
184
+ isPrimaryKey: false;
185
+ isAutoincrement: false;
186
+ hasRuntimeDefault: false;
187
+ enumValues: undefined;
188
+ baseColumn: never;
189
+ identity: undefined;
190
+ generated: undefined;
191
+ }, {}, {}>;
192
+ response: import("drizzle-orm/pg-core").PgColumn<{
193
+ name: "response";
194
+ tableName: "customer_reviews";
195
+ dataType: "string";
196
+ columnType: "PgText";
197
+ data: string;
198
+ driverParam: string;
199
+ notNull: false;
200
+ hasDefault: false;
201
+ isPrimaryKey: false;
202
+ isAutoincrement: false;
203
+ hasRuntimeDefault: false;
204
+ enumValues: [string, ...string[]];
205
+ baseColumn: never;
206
+ identity: undefined;
207
+ generated: undefined;
208
+ }, {}, {}>;
209
+ responseBy: import("drizzle-orm/pg-core").PgColumn<{
210
+ name: "response_by";
211
+ tableName: "customer_reviews";
212
+ dataType: "string";
213
+ columnType: "PgText";
214
+ data: string;
215
+ driverParam: string;
216
+ notNull: false;
217
+ hasDefault: false;
218
+ isPrimaryKey: false;
219
+ isAutoincrement: false;
220
+ hasRuntimeDefault: false;
221
+ enumValues: [string, ...string[]];
222
+ baseColumn: never;
223
+ identity: undefined;
224
+ generated: undefined;
225
+ }, {}, {}>;
226
+ responseAt: import("drizzle-orm/pg-core").PgColumn<{
227
+ name: "response_at";
228
+ tableName: "customer_reviews";
229
+ dataType: "date";
230
+ columnType: "PgTimestamp";
231
+ data: Date;
232
+ driverParam: string;
233
+ notNull: false;
234
+ hasDefault: false;
235
+ isPrimaryKey: false;
236
+ isAutoincrement: false;
237
+ hasRuntimeDefault: false;
238
+ enumValues: undefined;
239
+ baseColumn: never;
240
+ identity: undefined;
241
+ generated: undefined;
242
+ }, {}, {}>;
243
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
244
+ name: "created_at";
245
+ tableName: "customer_reviews";
246
+ dataType: "date";
247
+ columnType: "PgTimestamp";
248
+ data: Date;
249
+ driverParam: string;
250
+ notNull: true;
251
+ hasDefault: true;
252
+ isPrimaryKey: false;
253
+ isAutoincrement: false;
254
+ hasRuntimeDefault: false;
255
+ enumValues: undefined;
256
+ baseColumn: never;
257
+ identity: undefined;
258
+ generated: undefined;
259
+ }, {}, {}>;
260
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
261
+ name: "updated_at";
262
+ tableName: "customer_reviews";
263
+ dataType: "date";
264
+ columnType: "PgTimestamp";
265
+ data: Date;
266
+ driverParam: string;
267
+ notNull: true;
268
+ hasDefault: true;
269
+ isPrimaryKey: false;
270
+ isAutoincrement: false;
271
+ hasRuntimeDefault: false;
272
+ enumValues: undefined;
273
+ baseColumn: never;
274
+ identity: undefined;
275
+ generated: undefined;
276
+ }, {}, {}>;
277
+ };
278
+ dialect: "pg";
279
+ }>;
280
+ //# 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuBzB,CAAC"}
package/dist/schema.js ADDED
@@ -0,0 +1,25 @@
1
+ import { pgTable, uuid, text, integer, boolean, timestamp, index } from "drizzle-orm/pg-core";
2
+ export const customerReviews = pgTable("customer_reviews", {
3
+ id: uuid("id").defaultRandom().primaryKey(),
4
+ organizationId: text("organization_id").notNull(),
5
+ customerId: uuid("customer_id"),
6
+ entityId: uuid("entity_id").notNull(),
7
+ orderId: uuid("order_id"),
8
+ rating: integer("rating").notNull(),
9
+ title: text("title"),
10
+ body: text("body"),
11
+ status: text("status", { enum: ["pending", "approved", "rejected"] })
12
+ .notNull()
13
+ .default("pending"),
14
+ isVerified: boolean("is_verified").notNull().default(false),
15
+ isPublished: boolean("is_published").notNull().default(false),
16
+ response: text("response"),
17
+ responseBy: text("response_by"),
18
+ responseAt: timestamp("response_at", { withTimezone: true }),
19
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
20
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
21
+ }, (table) => ({
22
+ orgIdx: index("idx_customer_reviews_org").on(table.organizationId),
23
+ entityIdx: index("idx_customer_reviews_entity").on(table.entityId),
24
+ statusIdx: index("idx_customer_reviews_status").on(table.status),
25
+ }));
@@ -0,0 +1,26 @@
1
+ import type { PluginResult } from "@unifiedcommerce/core";
2
+ import type { Db, Review } from "../types";
3
+ export interface ReviewSummary {
4
+ averageRating: number;
5
+ totalCount: number;
6
+ distribution: Record<number, number>;
7
+ }
8
+ export declare class ReviewService {
9
+ private db;
10
+ constructor(db: Db);
11
+ submit(orgId: string, input: {
12
+ customerId?: string;
13
+ entityId: string;
14
+ orderId?: string;
15
+ rating: number;
16
+ title?: string;
17
+ body?: string;
18
+ }): Promise<PluginResult<Review>>;
19
+ listForEntity(orgId: string, entityId: string, publishedOnly?: boolean): Promise<PluginResult<Review[]>>;
20
+ getSummary(orgId: string, entityId: string): Promise<PluginResult<ReviewSummary>>;
21
+ approve(orgId: string, id: string): Promise<PluginResult<Review>>;
22
+ reject(orgId: string, id: string): Promise<PluginResult<Review>>;
23
+ reply(orgId: string, id: string, response: string, responseBy: string): Promise<PluginResult<Review>>;
24
+ listByCustomer(orgId: string, customerId: string): Promise<PluginResult<Review[]>>;
25
+ }
26
+ //# sourceMappingURL=review-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-service.d.ts","sourceRoot":"","sources":["../../src/services/review-service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAE3C,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED,qBAAa,aAAa;IACZ,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAEpB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QACjC,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IAkB3B,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC;IAYxG,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;IAwBjF,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IASjE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IAShE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IASrG,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC;CAQzF"}
@@ -0,0 +1,88 @@
1
+ import { eq, and, sql } from "drizzle-orm";
2
+ import { Ok, Err } from "@unifiedcommerce/core";
3
+ import { customerReviews } from "../schema";
4
+ export class ReviewService {
5
+ db;
6
+ constructor(db) {
7
+ this.db = db;
8
+ }
9
+ async submit(orgId, input) {
10
+ if (input.rating < 1 || input.rating > 5 || !Number.isInteger(input.rating)) {
11
+ return Err("Rating must be an integer between 1 and 5");
12
+ }
13
+ const isVerified = input.orderId != null;
14
+ const rows = await this.db.insert(customerReviews).values({
15
+ organizationId: orgId,
16
+ customerId: input.customerId ?? null,
17
+ entityId: input.entityId,
18
+ orderId: input.orderId ?? null,
19
+ rating: input.rating,
20
+ title: input.title ?? null,
21
+ body: input.body ?? null,
22
+ isVerified,
23
+ }).returning();
24
+ return Ok(rows[0]);
25
+ }
26
+ async listForEntity(orgId, entityId, publishedOnly) {
27
+ const conditions = [
28
+ eq(customerReviews.organizationId, orgId),
29
+ eq(customerReviews.entityId, entityId),
30
+ ];
31
+ if (publishedOnly) {
32
+ conditions.push(eq(customerReviews.isPublished, true));
33
+ }
34
+ const rows = await this.db.select().from(customerReviews).where(and(...conditions));
35
+ return Ok(rows);
36
+ }
37
+ async getSummary(orgId, entityId) {
38
+ const rows = await this.db.select({
39
+ rating: customerReviews.rating,
40
+ count: sql `count(*)::int`,
41
+ })
42
+ .from(customerReviews)
43
+ .where(and(eq(customerReviews.organizationId, orgId), eq(customerReviews.entityId, entityId)))
44
+ .groupBy(customerReviews.rating);
45
+ const distribution = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
46
+ let totalCount = 0;
47
+ let totalSum = 0;
48
+ for (const row of rows) {
49
+ distribution[row.rating] = row.count;
50
+ totalCount += row.count;
51
+ totalSum += row.rating * row.count;
52
+ }
53
+ const averageRating = totalCount > 0 ? Math.round((totalSum / totalCount) * 100) / 100 : 0;
54
+ return Ok({ averageRating, totalCount, distribution });
55
+ }
56
+ async approve(orgId, id) {
57
+ const rows = await this.db.update(customerReviews)
58
+ .set({ status: "approved", isPublished: true, updatedAt: new Date() })
59
+ .where(and(eq(customerReviews.organizationId, orgId), eq(customerReviews.id, id)))
60
+ .returning();
61
+ if (rows.length === 0)
62
+ return Err("Review not found");
63
+ return Ok(rows[0]);
64
+ }
65
+ async reject(orgId, id) {
66
+ const rows = await this.db.update(customerReviews)
67
+ .set({ status: "rejected", isPublished: false, updatedAt: new Date() })
68
+ .where(and(eq(customerReviews.organizationId, orgId), eq(customerReviews.id, id)))
69
+ .returning();
70
+ if (rows.length === 0)
71
+ return Err("Review not found");
72
+ return Ok(rows[0]);
73
+ }
74
+ async reply(orgId, id, response, responseBy) {
75
+ const rows = await this.db.update(customerReviews)
76
+ .set({ response, responseBy, responseAt: new Date(), updatedAt: new Date() })
77
+ .where(and(eq(customerReviews.organizationId, orgId), eq(customerReviews.id, id)))
78
+ .returning();
79
+ if (rows.length === 0)
80
+ return Err("Review not found");
81
+ return Ok(rows[0]);
82
+ }
83
+ async listByCustomer(orgId, customerId) {
84
+ const rows = await this.db.select().from(customerReviews)
85
+ .where(and(eq(customerReviews.organizationId, orgId), eq(customerReviews.customerId, customerId)));
86
+ return Ok(rows);
87
+ }
88
+ }