@unifiedcommerce/plugin-marketplace 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 (122) hide show
  1. package/README.md +479 -0
  2. package/dist/analytics-models.d.ts +13 -0
  3. package/dist/analytics-models.d.ts.map +1 -0
  4. package/dist/analytics-models.js +69 -0
  5. package/dist/hooks.d.ts +4 -0
  6. package/dist/hooks.d.ts.map +1 -0
  7. package/dist/hooks.js +187 -0
  8. package/dist/index.d.ts +4 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +105 -0
  11. package/dist/mcp-tools.d.ts +21 -0
  12. package/dist/mcp-tools.d.ts.map +1 -0
  13. package/dist/mcp-tools.js +183 -0
  14. package/dist/routes/b2b.d.ts +9 -0
  15. package/dist/routes/b2b.d.ts.map +1 -0
  16. package/dist/routes/b2b.js +156 -0
  17. package/dist/routes/commission.d.ts +6 -0
  18. package/dist/routes/commission.d.ts.map +1 -0
  19. package/dist/routes/commission.js +85 -0
  20. package/dist/routes/disputes-returns-reviews.d.ts +10 -0
  21. package/dist/routes/disputes-returns-reviews.d.ts.map +1 -0
  22. package/dist/routes/disputes-returns-reviews.js +179 -0
  23. package/dist/routes/payouts.d.ts +6 -0
  24. package/dist/routes/payouts.d.ts.map +1 -0
  25. package/dist/routes/payouts.js +40 -0
  26. package/dist/routes/sub-orders.d.ts +6 -0
  27. package/dist/routes/sub-orders.d.ts.map +1 -0
  28. package/dist/routes/sub-orders.js +44 -0
  29. package/dist/routes/util.d.ts +23 -0
  30. package/dist/routes/util.d.ts.map +1 -0
  31. package/dist/routes/util.js +41 -0
  32. package/dist/routes/vendor-portal.d.ts +14 -0
  33. package/dist/routes/vendor-portal.d.ts.map +1 -0
  34. package/dist/routes/vendor-portal.js +255 -0
  35. package/dist/routes/vendors.d.ts +11 -0
  36. package/dist/routes/vendors.d.ts.map +1 -0
  37. package/dist/routes/vendors.js +185 -0
  38. package/dist/schema.d.ts +3255 -0
  39. package/dist/schema.d.ts.map +1 -0
  40. package/dist/schema.js +225 -0
  41. package/dist/schemas/b2b.d.ts +1009 -0
  42. package/dist/schemas/b2b.d.ts.map +1 -0
  43. package/dist/schemas/b2b.js +208 -0
  44. package/dist/schemas/commission.d.ts +532 -0
  45. package/dist/schemas/commission.d.ts.map +1 -0
  46. package/dist/schemas/commission.js +113 -0
  47. package/dist/schemas/disputes-returns-reviews.d.ts +1405 -0
  48. package/dist/schemas/disputes-returns-reviews.d.ts.map +1 -0
  49. package/dist/schemas/disputes-returns-reviews.js +270 -0
  50. package/dist/schemas/payouts.d.ts +375 -0
  51. package/dist/schemas/payouts.d.ts.map +1 -0
  52. package/dist/schemas/payouts.js +78 -0
  53. package/dist/schemas/sub-orders.d.ts +303 -0
  54. package/dist/schemas/sub-orders.d.ts.map +1 -0
  55. package/dist/schemas/sub-orders.js +67 -0
  56. package/dist/schemas/vendor-portal.d.ts +1785 -0
  57. package/dist/schemas/vendor-portal.d.ts.map +1 -0
  58. package/dist/schemas/vendor-portal.js +294 -0
  59. package/dist/schemas/vendors.d.ts +1348 -0
  60. package/dist/schemas/vendors.d.ts.map +1 -0
  61. package/dist/schemas/vendors.js +245 -0
  62. package/dist/services/commission.d.ts +81 -0
  63. package/dist/services/commission.d.ts.map +1 -0
  64. package/dist/services/commission.js +98 -0
  65. package/dist/services/contract-price.d.ts +64 -0
  66. package/dist/services/contract-price.d.ts.map +1 -0
  67. package/dist/services/contract-price.js +57 -0
  68. package/dist/services/dispute.d.ts +156 -0
  69. package/dist/services/dispute.d.ts.map +1 -0
  70. package/dist/services/dispute.js +77 -0
  71. package/dist/services/payout.d.ts +126 -0
  72. package/dist/services/payout.d.ts.map +1 -0
  73. package/dist/services/payout.js +130 -0
  74. package/dist/services/return.d.ts +181 -0
  75. package/dist/services/return.d.ts.map +1 -0
  76. package/dist/services/return.js +80 -0
  77. package/dist/services/review.d.ts +70 -0
  78. package/dist/services/review.d.ts.map +1 -0
  79. package/dist/services/review.js +60 -0
  80. package/dist/services/rfq.d.ts +122 -0
  81. package/dist/services/rfq.d.ts.map +1 -0
  82. package/dist/services/rfq.js +60 -0
  83. package/dist/services/sub-order.d.ts +336 -0
  84. package/dist/services/sub-order.d.ts.map +1 -0
  85. package/dist/services/sub-order.js +121 -0
  86. package/dist/services/vendor.d.ts +528 -0
  87. package/dist/services/vendor.d.ts.map +1 -0
  88. package/dist/services/vendor.js +119 -0
  89. package/dist/types.d.ts +67 -0
  90. package/dist/types.d.ts.map +1 -0
  91. package/dist/types.js +13 -0
  92. package/package.json +43 -0
  93. package/src/analytics-models.ts +75 -0
  94. package/src/hooks.ts +215 -0
  95. package/src/index.ts +124 -0
  96. package/src/mcp-tools.ts +210 -0
  97. package/src/routes/b2b.ts +179 -0
  98. package/src/routes/commission.ts +95 -0
  99. package/src/routes/disputes-returns-reviews.ts +209 -0
  100. package/src/routes/payouts.ts +49 -0
  101. package/src/routes/sub-orders.ts +54 -0
  102. package/src/routes/util.ts +42 -0
  103. package/src/routes/vendor-portal.ts +277 -0
  104. package/src/routes/vendors.ts +201 -0
  105. package/src/schema.ts +260 -0
  106. package/src/schemas/b2b.ts +238 -0
  107. package/src/schemas/commission.ts +129 -0
  108. package/src/schemas/disputes-returns-reviews.ts +311 -0
  109. package/src/schemas/payouts.ts +90 -0
  110. package/src/schemas/sub-orders.ts +77 -0
  111. package/src/schemas/vendor-portal.ts +344 -0
  112. package/src/schemas/vendors.ts +281 -0
  113. package/src/services/commission.ts +120 -0
  114. package/src/services/contract-price.ts +80 -0
  115. package/src/services/dispute.ts +92 -0
  116. package/src/services/payout.ts +154 -0
  117. package/src/services/return.ts +92 -0
  118. package/src/services/review.ts +76 -0
  119. package/src/services/rfq.ts +82 -0
  120. package/src/services/sub-order.ts +136 -0
  121. package/src/services/vendor.ts +151 -0
  122. package/src/types.ts +164 -0
@@ -0,0 +1,49 @@
1
+ import { router } from "@unifiedcommerce/core";
2
+ import type { PluginRouteRegistration } from "@unifiedcommerce/core";
3
+ import type { PayoutService } from "../services/payout";
4
+ import { stripUndefined } from "./util";
5
+
6
+ export function buildPayoutRoutes(services: {
7
+ payout: PayoutService;
8
+ }): PluginRouteRegistration[] {
9
+ const r = router("Marketplace - Payouts", "/marketplace/payouts");
10
+
11
+ // ─── List payouts ──────────────────────────────────────────────────────────
12
+ r.get("/")
13
+ .summary("List payouts")
14
+ .permission("marketplace:admin")
15
+ .handler(async ({ query }) => {
16
+ return services.payout.listPayouts(stripUndefined({
17
+ vendorId: query.vendorId as string | undefined,
18
+ status: query.status as string | undefined,
19
+ }));
20
+ });
21
+
22
+ // ─── Run payout cycle ──────────────────────────────────────────────────────
23
+ r.post("/run")
24
+ .summary("Run a payout cycle")
25
+ .permission("marketplace:admin")
26
+ .handler(async () => {
27
+ return services.payout.runPayoutCycle();
28
+ });
29
+
30
+ // ─── Retry payout ──────────────────────────────────────────────────────────
31
+ r.post("/{id}/retry")
32
+ .summary("Retry a failed payout")
33
+ .permission("marketplace:admin")
34
+ .handler(async ({ params }) => {
35
+ return services.payout.retryPayout(params.id!);
36
+ });
37
+
38
+ // ─── Get payout by id ──────────────────────────────────────────────────────
39
+ r.get("/{id}")
40
+ .summary("Get payout by ID")
41
+ .permission("marketplace:admin")
42
+ .handler(async ({ params }) => {
43
+ const payout = await services.payout.getPayoutById(params.id!);
44
+ if (!payout) throw new Error("Payout not found");
45
+ return payout;
46
+ });
47
+
48
+ return r.routes();
49
+ }
@@ -0,0 +1,54 @@
1
+ import { router } from "@unifiedcommerce/core";
2
+ import type { PluginRouteRegistration } from "@unifiedcommerce/core";
3
+ import type { z } from "@hono/zod-openapi";
4
+ import type { SubOrderService } from "../services/sub-order";
5
+ import type { SubOrderStatus } from "../types";
6
+ import { UpdateSubOrderStatusBodySchema } from "../schemas/sub-orders";
7
+ import { stripUndefined } from "./util";
8
+
9
+ export function buildSubOrderRoutes(services: {
10
+ subOrder: SubOrderService;
11
+ }): PluginRouteRegistration[] {
12
+ const r = router("Marketplace - Sub-Orders", "/marketplace/sub-orders");
13
+
14
+ // ─── List sub-orders ───────────────────────────────────────────────────────
15
+ r.get("/")
16
+ .summary("List sub-orders")
17
+ .permission("marketplace:admin")
18
+ .handler(async ({ query }) => {
19
+ return services.subOrder.list(stripUndefined({
20
+ orderId: query.orderId as string | undefined,
21
+ vendorId: query.vendorId as string | undefined,
22
+ status: query.status as string | undefined,
23
+ }));
24
+ });
25
+
26
+ // ─── Get sub-order by id ───────────────────────────────────────────────────
27
+ r.get("/{id}")
28
+ .summary("Get sub-order by ID")
29
+ .permission("marketplace:admin")
30
+ .handler(async ({ params }) => {
31
+ const subOrder = await services.subOrder.getById(params.id!);
32
+ if (!subOrder) throw new Error("Sub-order not found");
33
+ return subOrder;
34
+ });
35
+
36
+ // ─── Force status change ───────────────────────────────────────────────────
37
+ r.patch("/{id}/status")
38
+ .summary("Force a sub-order status change")
39
+ .permission("marketplace:admin")
40
+ .input(UpdateSubOrderStatusBodySchema)
41
+ .handler(async ({ params, input }) => {
42
+ const body = input as z.infer<typeof UpdateSubOrderStatusBodySchema>;
43
+ const subOrder = await services.subOrder.getById(params.id!);
44
+ if (!subOrder) throw new Error("Sub-order not found");
45
+
46
+ // Use cancel() for cancelled status to trigger side effects
47
+ // (inventory release + ledger reversal)
48
+ return body.status === "cancelled"
49
+ ? services.subOrder.cancel(params.id!, "Admin force cancel")
50
+ : services.subOrder.forceStatus(params.id!, body.status as SubOrderStatus);
51
+ });
52
+
53
+ return r.routes();
54
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Removes keys whose value is `undefined` from an object, returning a
3
+ * cleaned copy that satisfies `exactOptionalPropertyTypes`.
4
+ *
5
+ * Zod's `.optional()` produces `T | undefined` in inferred types, which
6
+ * can't be assigned to optional properties under `exactOptionalPropertyTypes`.
7
+ * This helper strips those `undefined` entries at runtime and casts the
8
+ * result so TypeScript accepts it.
9
+ */
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ export function stripUndefined<T>(obj: T): T extends Record<string, any> ? { [K in keyof T]: Exclude<T[K], undefined> } : T {
12
+ if (obj == null || typeof obj !== "object") return obj as never;
13
+ const result: Record<string, unknown> = {};
14
+ for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
15
+ if (value !== undefined) {
16
+ result[key] = value;
17
+ }
18
+ }
19
+ return result as never;
20
+ }
21
+
22
+ /** Keys that must never be exposed in API responses. */
23
+ const VENDOR_SECRET_KEYS = [
24
+ "storeAccessToken",
25
+ "storeConsumerSecret",
26
+ "storeWebhookSecret",
27
+ "bankAccount",
28
+ ] as const;
29
+
30
+ /**
31
+ * Returns a copy of a vendor record with sensitive fields removed.
32
+ *
33
+ * Accepts any object with string keys so it works with both single vendor
34
+ * rows and Drizzle `InferSelectModel` types without importing the schema.
35
+ */
36
+ export function stripVendorSecrets<T extends Record<string, unknown>>(vendor: T): Omit<T, (typeof VENDOR_SECRET_KEYS)[number]> {
37
+ const copy = { ...vendor };
38
+ for (const key of VENDOR_SECRET_KEYS) {
39
+ delete copy[key];
40
+ }
41
+ return copy as Omit<T, (typeof VENDOR_SECRET_KEYS)[number]>;
42
+ }
@@ -0,0 +1,277 @@
1
+ import { router } from "@unifiedcommerce/core";
2
+ import type { PluginRouteRegistration } from "@unifiedcommerce/core";
3
+ import type { z } from "@hono/zod-openapi";
4
+ import type { VendorService } from "../services/vendor";
5
+ import type { SubOrderService } from "../services/sub-order";
6
+ import type { PayoutService } from "../services/payout";
7
+ import type { ReviewService } from "../services/review";
8
+ import type { ReturnService } from "../services/return";
9
+ import { vendorEntities } from "../schema";
10
+ import { eq } from "drizzle-orm";
11
+ import {
12
+ UpdateVendorProfileBodySchema,
13
+ UploadVendorDocumentBodySchema,
14
+ ShipSubOrderBodySchema,
15
+ CancelSubOrderBodySchema,
16
+ RespondToReviewBodySchema,
17
+ ApproveReturnBodySchema,
18
+ RejectReturnBodySchema,
19
+ } from "../schemas/vendor-portal";
20
+ import { stripUndefined, stripVendorSecrets } from "./util";
21
+
22
+ /** Helper to extract vendorId from actor and throw if missing. */
23
+ function requireVendorId(actor: { vendorId?: string | null } | null): string {
24
+ const vendorId = actor?.vendorId;
25
+ if (!vendorId) throw new Error("Forbidden");
26
+ return vendorId;
27
+ }
28
+
29
+ export function buildVendorPortalRoutes(services: {
30
+ vendor: VendorService;
31
+ subOrder: SubOrderService;
32
+ payout: PayoutService;
33
+ review: ReviewService;
34
+ return: ReturnService;
35
+ }): PluginRouteRegistration[] {
36
+ const r = router("Marketplace - Vendor Portal", "/marketplace/vendor/me");
37
+
38
+ // ─── Get my vendor profile ─────────────────────────────────────────────────
39
+ r.get("/")
40
+ .summary("Get my vendor profile")
41
+ .auth()
42
+ .handler(async ({ actor }) => {
43
+ const vendorId = requireVendorId(actor);
44
+ const vendor = await services.vendor.getById(vendorId);
45
+ if (!vendor) throw new Error("Vendor not found");
46
+ return stripVendorSecrets(vendor);
47
+ });
48
+
49
+ // ─── Update my vendor profile ──────────────────────────────────────────────
50
+ r.patch("/")
51
+ .summary("Update my vendor profile")
52
+ .auth()
53
+ .input(UpdateVendorProfileBodySchema)
54
+ .handler(async ({ actor, input }) => {
55
+ const vendorId = requireVendorId(actor);
56
+ const body = input as z.infer<typeof UpdateVendorProfileBodySchema>;
57
+ const updated = await services.vendor.update(vendorId, stripUndefined(body));
58
+ if (!updated) throw new Error("Vendor not found");
59
+ return stripVendorSecrets(updated);
60
+ });
61
+
62
+ // ─── Upload document ───────────────────────────────────────────────────────
63
+ r.post("/documents")
64
+ .summary("Upload a document for my vendor profile")
65
+ .auth()
66
+ .input(UploadVendorDocumentBodySchema)
67
+ .handler(async ({ actor, input }) => {
68
+ const vendorId = requireVendorId(actor);
69
+ const body = input as z.infer<typeof UploadVendorDocumentBodySchema>;
70
+ return services.vendor.uploadDocument(vendorId, {
71
+ type: body.type,
72
+ fileUrl: body.fileUrl,
73
+ });
74
+ });
75
+
76
+ // ─── List my documents ─────────────────────────────────────────────────────
77
+ r.get("/documents")
78
+ .summary("List my vendor documents")
79
+ .auth()
80
+ .handler(async ({ actor }) => {
81
+ const vendorId = requireVendorId(actor);
82
+ return services.vendor.listDocuments(vendorId);
83
+ });
84
+
85
+ // ─── List my products ──────────────────────────────────────────────────────
86
+ r.get("/products")
87
+ .summary("List my products")
88
+ .auth()
89
+ .handler(async ({ actor, db }) => {
90
+ const vendorId = requireVendorId(actor);
91
+ const drizzle = db as import("../types").Db;
92
+ const entities = await drizzle
93
+ .select()
94
+ .from(vendorEntities)
95
+ .where(eq(vendorEntities.vendorId, vendorId));
96
+ return entities;
97
+ });
98
+
99
+ // ─── List my sub-orders ────────────────────────────────────────────────────
100
+ r.get("/orders")
101
+ .summary("List my sub-orders")
102
+ .auth()
103
+ .handler(async ({ actor, query }) => {
104
+ const vendorId = requireVendorId(actor);
105
+ const status = query.status as string | undefined;
106
+ return services.subOrder.listByVendor(vendorId, stripUndefined({ status }));
107
+ });
108
+
109
+ // ─── Get single sub-order ──────────────────────────────────────────────────
110
+ r.get("/orders/{subOrderId}")
111
+ .summary("Get a single sub-order")
112
+ .auth()
113
+ .handler(async ({ actor, params }) => {
114
+ const vendorId = requireVendorId(actor);
115
+ const subOrder = await services.subOrder.getById(params.subOrderId!);
116
+ if (!subOrder) throw new Error("Sub-order not found");
117
+ if (subOrder.vendorId !== vendorId) throw new Error("Forbidden");
118
+ return subOrder;
119
+ });
120
+
121
+ // ─── Confirm sub-order ─────────────────────────────────────────────────────
122
+ r.post("/orders/{subOrderId}/confirm")
123
+ .summary("Confirm a sub-order")
124
+ .auth()
125
+ .handler(async ({ actor, params }) => {
126
+ const vendorId = requireVendorId(actor);
127
+ const subOrder = await services.subOrder.getById(params.subOrderId!);
128
+ if (!subOrder) throw new Error("Sub-order not found");
129
+ if (subOrder.vendorId !== vendorId) throw new Error("Forbidden");
130
+ return services.subOrder.confirm(params.subOrderId!);
131
+ });
132
+
133
+ // ─── Ship sub-order ────────────────────────────────────────────────────────
134
+ r.post("/orders/{subOrderId}/ship")
135
+ .summary("Ship a sub-order")
136
+ .auth()
137
+ .input(ShipSubOrderBodySchema)
138
+ .handler(async ({ actor, params, input }) => {
139
+ const vendorId = requireVendorId(actor);
140
+ const body = input as z.infer<typeof ShipSubOrderBodySchema>;
141
+ const subOrder = await services.subOrder.getById(params.subOrderId!);
142
+ if (!subOrder) throw new Error("Sub-order not found");
143
+ if (subOrder.vendorId !== vendorId) throw new Error("Forbidden");
144
+ return services.subOrder.ship(params.subOrderId!, {
145
+ trackingNumber: body.trackingNumber,
146
+ carrier: body.carrier,
147
+ });
148
+ });
149
+
150
+ // ─── Deliver sub-order ─────────────────────────────────────────────────────
151
+ r.post("/orders/{subOrderId}/deliver")
152
+ .summary("Mark a sub-order as delivered")
153
+ .auth()
154
+ .handler(async ({ actor, params }) => {
155
+ const vendorId = requireVendorId(actor);
156
+ const subOrder = await services.subOrder.getById(params.subOrderId!);
157
+ if (!subOrder) throw new Error("Sub-order not found");
158
+ if (subOrder.vendorId !== vendorId) throw new Error("Forbidden");
159
+ return services.subOrder.deliver(params.subOrderId!);
160
+ });
161
+
162
+ // ─── Cancel sub-order ──────────────────────────────────────────────────────
163
+ r.post("/orders/{subOrderId}/cancel")
164
+ .summary("Cancel a sub-order")
165
+ .auth()
166
+ .input(CancelSubOrderBodySchema)
167
+ .handler(async ({ actor, params, input }) => {
168
+ const vendorId = requireVendorId(actor);
169
+ const body = input as z.infer<typeof CancelSubOrderBodySchema>;
170
+ const subOrder = await services.subOrder.getById(params.subOrderId!);
171
+ if (!subOrder) throw new Error("Sub-order not found");
172
+ if (subOrder.vendorId !== vendorId) throw new Error("Forbidden");
173
+ return services.subOrder.cancel(params.subOrderId!, body.reason);
174
+ });
175
+
176
+ // ─── List my payouts ───────────────────────────────────────────────────────
177
+ r.get("/payouts")
178
+ .summary("List my payouts")
179
+ .auth()
180
+ .handler(async ({ actor }) => {
181
+ const vendorId = requireVendorId(actor);
182
+ return services.payout.listPayouts({ vendorId });
183
+ });
184
+
185
+ // ─── My balance ────────────────────────────────────────────────────────────
186
+ r.get("/balance")
187
+ .summary("Get my balance")
188
+ .auth()
189
+ .handler(async ({ actor }) => {
190
+ const vendorId = requireVendorId(actor);
191
+ const balance = await services.payout.getBalance(vendorId);
192
+ const ledger = await services.payout.getLedger(vendorId);
193
+ return { balanceCents: balance, ledger };
194
+ });
195
+
196
+ // ─── My analytics ──────────────────────────────────────────────────────────
197
+ r.get("/analytics")
198
+ .summary("Get my analytics")
199
+ .auth()
200
+ .handler(async ({ actor }) => {
201
+ const vendorId = requireVendorId(actor);
202
+ const rating = await services.review.getAggregateRating(vendorId);
203
+ const balance = await services.payout.getBalance(vendorId);
204
+ return { rating, balanceCents: balance };
205
+ });
206
+
207
+ // ─── My reviews ────────────────────────────────────────────────────────────
208
+ r.get("/reviews")
209
+ .summary("List my reviews")
210
+ .auth()
211
+ .handler(async ({ actor }) => {
212
+ const vendorId = requireVendorId(actor);
213
+ return services.review.getForVendor(vendorId, true);
214
+ });
215
+
216
+ // ─── Respond to review ─────────────────────────────────────────────────────
217
+ r.post("/reviews/{id}/respond")
218
+ .summary("Respond to a review")
219
+ .auth()
220
+ .input(RespondToReviewBodySchema)
221
+ .handler(async ({ actor, params, input }) => {
222
+ const vendorId = requireVendorId(actor);
223
+ const body = input as z.infer<typeof RespondToReviewBodySchema>;
224
+ // IDOR prevention: verify the review belongs to this vendor
225
+ const vendorReviews = await services.review.getForVendor(vendorId, true);
226
+ const ownsReview = vendorReviews.some((rev: { id: string }) => rev.id === params.id!);
227
+ if (!ownsReview) throw new Error("Forbidden: review does not belong to this vendor");
228
+ const updated = await services.review.respond(params.id!, body.response);
229
+ if (!updated) throw new Error("Review not found");
230
+ return updated;
231
+ });
232
+
233
+ // ─── My returns ────────────────────────────────────────────────────────────
234
+ r.get("/returns")
235
+ .summary("List my returns")
236
+ .auth()
237
+ .handler(async ({ actor }) => {
238
+ const vendorId = requireVendorId(actor);
239
+ return services.return.listByVendor(vendorId);
240
+ });
241
+
242
+ // ─── Approve return ────────────────────────────────────────────────────────
243
+ r.post("/returns/{id}/approve")
244
+ .summary("Approve a return request")
245
+ .auth()
246
+ .input(ApproveReturnBodySchema)
247
+ .handler(async ({ actor, params, input }) => {
248
+ const vendorId = requireVendorId(actor);
249
+ const ret = await services.return.getById(params.id!);
250
+ if (!ret) throw new Error("Return not found");
251
+ const subOrder = await services.subOrder.getById(ret.subOrderId);
252
+ if (!subOrder || subOrder.vendorId !== vendorId) throw new Error("Forbidden");
253
+ const body = input as z.infer<typeof ApproveReturnBodySchema>;
254
+ const updated = await services.return.vendorApprove(params.id!, body.refundAmountCents);
255
+ if (!updated) throw new Error("Return not found");
256
+ return updated;
257
+ });
258
+
259
+ // ─── Reject return ─────────────────────────────────────────────────────────
260
+ r.post("/returns/{id}/reject")
261
+ .summary("Reject a return request")
262
+ .auth()
263
+ .input(RejectReturnBodySchema)
264
+ .handler(async ({ actor, params, input }) => {
265
+ const vendorId = requireVendorId(actor);
266
+ const ret = await services.return.getById(params.id!);
267
+ if (!ret) throw new Error("Return not found");
268
+ const subOrder = await services.subOrder.getById(ret.subOrderId);
269
+ if (!subOrder || subOrder.vendorId !== vendorId) throw new Error("Forbidden");
270
+ const body = input as z.infer<typeof RejectReturnBodySchema>;
271
+ const updated = await services.return.vendorReject(params.id!, body.notes);
272
+ if (!updated) throw new Error("Return not found");
273
+ return updated;
274
+ });
275
+
276
+ return r.routes();
277
+ }
@@ -0,0 +1,201 @@
1
+ import { router, resolveOrgId } from "@unifiedcommerce/core";
2
+ import type { PluginRouteRegistration } from "@unifiedcommerce/core";
3
+ import type { z } from "@hono/zod-openapi";
4
+ import type { VendorService } from "../services/vendor";
5
+ import type { PayoutService } from "../services/payout";
6
+ import type { ReviewService } from "../services/review";
7
+ import type { MarketplacePluginOptions } from "../types";
8
+ import {
9
+ CreateVendorBodySchema,
10
+ UpdateVendorBodySchema,
11
+ RejectVendorBodySchema,
12
+ SuspendVendorBodySchema,
13
+ UploadDocumentBodySchema,
14
+ } from "../schemas/vendors";
15
+ import { stripUndefined, stripVendorSecrets } from "./util";
16
+
17
+ export function buildVendorRoutes(services: {
18
+ vendor: VendorService;
19
+ payout: PayoutService;
20
+ review: ReviewService;
21
+ }, options?: MarketplacePluginOptions): PluginRouteRegistration[] {
22
+ const r = router("Marketplace - Vendors", "/marketplace/vendors");
23
+
24
+ // ─── List vendors ──────────────────────────────────────────────────────────
25
+ r.get("/")
26
+ .summary("List all vendors")
27
+ .permission("marketplace:admin")
28
+ .handler(async ({ query }) => {
29
+ const vendors = await services.vendor.list(stripUndefined({
30
+ status: query.status as string | undefined,
31
+ tier: query.tier as string | undefined,
32
+ search: query.search as string | undefined,
33
+ }));
34
+ return vendors.map(stripVendorSecrets);
35
+ });
36
+
37
+ // ─── Create vendor ─────────────────────────────────────────────────────────
38
+ r.post("/")
39
+ .summary("Create a vendor")
40
+ .permission("marketplace:admin")
41
+ .input(CreateVendorBodySchema)
42
+ .handler(async ({ input, actor }) => {
43
+ const body = input as z.infer<typeof CreateVendorBodySchema>;
44
+ const defaultBps = options?.defaultCommissionRateBps ?? 1000;
45
+ return services.vendor.create(resolveOrgId(actor), stripUndefined({
46
+ ...body,
47
+ commissionRateBps: body.commissionRateBps ?? defaultBps,
48
+ }));
49
+ });
50
+
51
+ // ─── Get vendor detail ─────────────────────────────────────────────────────
52
+ r.get("/{id}")
53
+ .summary("Get vendor detail")
54
+ .permission("marketplace:admin")
55
+ .handler(async ({ params }) => {
56
+ const vendor = await services.vendor.getById(params.id!);
57
+ if (!vendor) throw new Error("Vendor not found");
58
+ return stripVendorSecrets(vendor);
59
+ });
60
+
61
+ // ─── Update vendor ─────────────────────────────────────────────────────────
62
+ r.patch("/{id}")
63
+ .summary("Update a vendor")
64
+ .permission("marketplace:admin")
65
+ .input(UpdateVendorBodySchema)
66
+ .handler(async ({ params, input }) => {
67
+ const body = input as z.infer<typeof UpdateVendorBodySchema>;
68
+ const updated = await services.vendor.update(params.id!, stripUndefined(body));
69
+ if (!updated) throw new Error("Vendor not found");
70
+ return stripVendorSecrets(updated);
71
+ });
72
+
73
+ // ─── Approve vendor ────────────────────────────────────────────────────────
74
+ r.post("/{id}/approve")
75
+ .summary("Approve a vendor application")
76
+ .permission("marketplace:admin")
77
+ .handler(async ({ params }) => {
78
+ const vendor = await services.vendor.getById(params.id!);
79
+ if (!vendor) throw new Error("Vendor not found");
80
+ const approved = await services.vendor.approve(params.id!);
81
+ if (!approved) throw new Error("Vendor not found");
82
+ return stripVendorSecrets(approved);
83
+ });
84
+
85
+ // ─── Reject vendor ─────────────────────────────────────────────────────────
86
+ r.post("/{id}/reject")
87
+ .summary("Reject a vendor application")
88
+ .permission("marketplace:admin")
89
+ .input(RejectVendorBodySchema)
90
+ .handler(async ({ params, input }) => {
91
+ const body = input as z.infer<typeof RejectVendorBodySchema>;
92
+ const vendor = await services.vendor.getById(params.id!);
93
+ if (!vendor) throw new Error("Vendor not found");
94
+ const rejected = await services.vendor.reject(params.id!, body.reason);
95
+ if (!rejected) throw new Error("Vendor not found");
96
+ return stripVendorSecrets(rejected);
97
+ });
98
+
99
+ // ─── Suspend vendor ────────────────────────────────────────────────────────
100
+ r.post("/{id}/suspend")
101
+ .summary("Suspend a vendor")
102
+ .permission("marketplace:admin")
103
+ .input(SuspendVendorBodySchema)
104
+ .handler(async ({ params, input }) => {
105
+ const body = input as z.infer<typeof SuspendVendorBodySchema>;
106
+ const vendor = await services.vendor.getById(params.id!);
107
+ if (!vendor) throw new Error("Vendor not found");
108
+ const suspended = await services.vendor.suspend(params.id!, body.reason);
109
+ if (!suspended) throw new Error("Vendor not found");
110
+ return stripVendorSecrets(suspended);
111
+ });
112
+
113
+ // ─── Reinstate vendor ──────────────────────────────────────────────────────
114
+ r.post("/{id}/reinstate")
115
+ .summary("Reinstate a suspended vendor")
116
+ .permission("marketplace:admin")
117
+ .handler(async ({ params }) => {
118
+ const vendor = await services.vendor.getById(params.id!);
119
+ if (!vendor) throw new Error("Vendor not found");
120
+ const reinstated = await services.vendor.reinstate(params.id!);
121
+ if (!reinstated) throw new Error("Vendor not found");
122
+ return stripVendorSecrets(reinstated);
123
+ });
124
+
125
+ // ─── Upload vendor document ────────────────────────────────────────────────
126
+ r.post("/{id}/documents")
127
+ .summary("Upload a vendor document")
128
+ .permission("marketplace:admin")
129
+ .input(UploadDocumentBodySchema)
130
+ .handler(async ({ params, input }) => {
131
+ const body = input as z.infer<typeof UploadDocumentBodySchema>;
132
+ const vendor = await services.vendor.getById(params.id!);
133
+ if (!vendor) throw new Error("Vendor not found");
134
+ return services.vendor.uploadDocument(params.id!, {
135
+ type: body.type,
136
+ fileUrl: body.fileUrl,
137
+ });
138
+ });
139
+
140
+ // ─── List vendor documents ─────────────────────────────────────────────────
141
+ r.get("/{id}/documents")
142
+ .summary("List vendor documents")
143
+ .permission("marketplace:admin")
144
+ .handler(async ({ params }) => {
145
+ const vendor = await services.vendor.getById(params.id!);
146
+ if (!vendor) throw new Error("Vendor not found");
147
+ return services.vendor.listDocuments(params.id!);
148
+ });
149
+
150
+ // ─── Approve document ──────────────────────────────────────────────────────
151
+ r.post("/{id}/documents/{docId}/approve")
152
+ .summary("Approve a vendor document")
153
+ .permission("marketplace:admin")
154
+ .handler(async ({ params }) => {
155
+ const updated = await services.vendor.approveDocument(params.docId!);
156
+ if (!updated) throw new Error("Document not found");
157
+ return updated;
158
+ });
159
+
160
+ // ─── Reject document ───────────────────────────────────────────────────────
161
+ r.post("/{id}/documents/{docId}/reject")
162
+ .summary("Reject a vendor document")
163
+ .permission("marketplace:admin")
164
+ .handler(async ({ params }) => {
165
+ const updated = await services.vendor.rejectDocument(params.docId!);
166
+ if (!updated) throw new Error("Document not found");
167
+ return updated;
168
+ });
169
+
170
+ // ─── Vendor balance ────────────────────────────────────────────────────────
171
+ r.get("/{id}/balance")
172
+ .summary("Get vendor balance")
173
+ .permission("marketplace:admin")
174
+ .handler(async ({ params }) => {
175
+ const vendor = await services.vendor.getById(params.id!);
176
+ if (!vendor) throw new Error("Vendor not found");
177
+ const balance = await services.payout.getBalance(params.id!);
178
+ const ledger = await services.payout.getLedger(params.id!);
179
+ return { balanceCents: balance, ledger };
180
+ });
181
+
182
+ // ─── Vendor performance ────────────────────────────────────────────────────
183
+ r.get("/{id}/performance")
184
+ .summary("Get vendor performance metrics")
185
+ .permission("marketplace:admin")
186
+ .handler(async ({ params }) => {
187
+ const vendor = await services.vendor.getById(params.id!);
188
+ if (!vendor) throw new Error("Vendor not found");
189
+ const rating = await services.review.getAggregateRating(params.id!);
190
+ const balance = await services.payout.getBalance(params.id!);
191
+ return {
192
+ vendorId: params.id!,
193
+ performanceScore: vendor.performanceScore,
194
+ tier: vendor.tier,
195
+ rating,
196
+ balanceCents: balance,
197
+ };
198
+ });
199
+
200
+ return r.routes();
201
+ }