@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,255 @@
1
+ import { router } from "@unifiedcommerce/core";
2
+ import { vendorEntities } from "../schema";
3
+ import { eq } from "drizzle-orm";
4
+ import { UpdateVendorProfileBodySchema, UploadVendorDocumentBodySchema, ShipSubOrderBodySchema, CancelSubOrderBodySchema, RespondToReviewBodySchema, ApproveReturnBodySchema, RejectReturnBodySchema, } from "../schemas/vendor-portal";
5
+ import { stripUndefined, stripVendorSecrets } from "./util";
6
+ /** Helper to extract vendorId from actor and throw if missing. */
7
+ function requireVendorId(actor) {
8
+ const vendorId = actor?.vendorId;
9
+ if (!vendorId)
10
+ throw new Error("Forbidden");
11
+ return vendorId;
12
+ }
13
+ export function buildVendorPortalRoutes(services) {
14
+ const r = router("Marketplace - Vendor Portal", "/marketplace/vendor/me");
15
+ // ─── Get my vendor profile ─────────────────────────────────────────────────
16
+ r.get("/")
17
+ .summary("Get my vendor profile")
18
+ .auth()
19
+ .handler(async ({ actor }) => {
20
+ const vendorId = requireVendorId(actor);
21
+ const vendor = await services.vendor.getById(vendorId);
22
+ if (!vendor)
23
+ throw new Error("Vendor not found");
24
+ return stripVendorSecrets(vendor);
25
+ });
26
+ // ─── Update my vendor profile ──────────────────────────────────────────────
27
+ r.patch("/")
28
+ .summary("Update my vendor profile")
29
+ .auth()
30
+ .input(UpdateVendorProfileBodySchema)
31
+ .handler(async ({ actor, input }) => {
32
+ const vendorId = requireVendorId(actor);
33
+ const body = input;
34
+ const updated = await services.vendor.update(vendorId, stripUndefined(body));
35
+ if (!updated)
36
+ throw new Error("Vendor not found");
37
+ return stripVendorSecrets(updated);
38
+ });
39
+ // ─── Upload document ───────────────────────────────────────────────────────
40
+ r.post("/documents")
41
+ .summary("Upload a document for my vendor profile")
42
+ .auth()
43
+ .input(UploadVendorDocumentBodySchema)
44
+ .handler(async ({ actor, input }) => {
45
+ const vendorId = requireVendorId(actor);
46
+ const body = input;
47
+ return services.vendor.uploadDocument(vendorId, {
48
+ type: body.type,
49
+ fileUrl: body.fileUrl,
50
+ });
51
+ });
52
+ // ─── List my documents ─────────────────────────────────────────────────────
53
+ r.get("/documents")
54
+ .summary("List my vendor documents")
55
+ .auth()
56
+ .handler(async ({ actor }) => {
57
+ const vendorId = requireVendorId(actor);
58
+ return services.vendor.listDocuments(vendorId);
59
+ });
60
+ // ─── List my products ──────────────────────────────────────────────────────
61
+ r.get("/products")
62
+ .summary("List my products")
63
+ .auth()
64
+ .handler(async ({ actor, db }) => {
65
+ const vendorId = requireVendorId(actor);
66
+ const drizzle = db;
67
+ const entities = await drizzle
68
+ .select()
69
+ .from(vendorEntities)
70
+ .where(eq(vendorEntities.vendorId, vendorId));
71
+ return entities;
72
+ });
73
+ // ─── List my sub-orders ────────────────────────────────────────────────────
74
+ r.get("/orders")
75
+ .summary("List my sub-orders")
76
+ .auth()
77
+ .handler(async ({ actor, query }) => {
78
+ const vendorId = requireVendorId(actor);
79
+ const status = query.status;
80
+ return services.subOrder.listByVendor(vendorId, stripUndefined({ status }));
81
+ });
82
+ // ─── Get single sub-order ──────────────────────────────────────────────────
83
+ r.get("/orders/{subOrderId}")
84
+ .summary("Get a single sub-order")
85
+ .auth()
86
+ .handler(async ({ actor, params }) => {
87
+ const vendorId = requireVendorId(actor);
88
+ const subOrder = await services.subOrder.getById(params.subOrderId);
89
+ if (!subOrder)
90
+ throw new Error("Sub-order not found");
91
+ if (subOrder.vendorId !== vendorId)
92
+ throw new Error("Forbidden");
93
+ return subOrder;
94
+ });
95
+ // ─── Confirm sub-order ─────────────────────────────────────────────────────
96
+ r.post("/orders/{subOrderId}/confirm")
97
+ .summary("Confirm a sub-order")
98
+ .auth()
99
+ .handler(async ({ actor, params }) => {
100
+ const vendorId = requireVendorId(actor);
101
+ const subOrder = await services.subOrder.getById(params.subOrderId);
102
+ if (!subOrder)
103
+ throw new Error("Sub-order not found");
104
+ if (subOrder.vendorId !== vendorId)
105
+ throw new Error("Forbidden");
106
+ return services.subOrder.confirm(params.subOrderId);
107
+ });
108
+ // ─── Ship sub-order ────────────────────────────────────────────────────────
109
+ r.post("/orders/{subOrderId}/ship")
110
+ .summary("Ship a sub-order")
111
+ .auth()
112
+ .input(ShipSubOrderBodySchema)
113
+ .handler(async ({ actor, params, input }) => {
114
+ const vendorId = requireVendorId(actor);
115
+ const body = input;
116
+ const subOrder = await services.subOrder.getById(params.subOrderId);
117
+ if (!subOrder)
118
+ throw new Error("Sub-order not found");
119
+ if (subOrder.vendorId !== vendorId)
120
+ throw new Error("Forbidden");
121
+ return services.subOrder.ship(params.subOrderId, {
122
+ trackingNumber: body.trackingNumber,
123
+ carrier: body.carrier,
124
+ });
125
+ });
126
+ // ─── Deliver sub-order ─────────────────────────────────────────────────────
127
+ r.post("/orders/{subOrderId}/deliver")
128
+ .summary("Mark a sub-order as delivered")
129
+ .auth()
130
+ .handler(async ({ actor, params }) => {
131
+ const vendorId = requireVendorId(actor);
132
+ const subOrder = await services.subOrder.getById(params.subOrderId);
133
+ if (!subOrder)
134
+ throw new Error("Sub-order not found");
135
+ if (subOrder.vendorId !== vendorId)
136
+ throw new Error("Forbidden");
137
+ return services.subOrder.deliver(params.subOrderId);
138
+ });
139
+ // ─── Cancel sub-order ──────────────────────────────────────────────────────
140
+ r.post("/orders/{subOrderId}/cancel")
141
+ .summary("Cancel a sub-order")
142
+ .auth()
143
+ .input(CancelSubOrderBodySchema)
144
+ .handler(async ({ actor, params, input }) => {
145
+ const vendorId = requireVendorId(actor);
146
+ const body = input;
147
+ const subOrder = await services.subOrder.getById(params.subOrderId);
148
+ if (!subOrder)
149
+ throw new Error("Sub-order not found");
150
+ if (subOrder.vendorId !== vendorId)
151
+ throw new Error("Forbidden");
152
+ return services.subOrder.cancel(params.subOrderId, body.reason);
153
+ });
154
+ // ─── List my payouts ───────────────────────────────────────────────────────
155
+ r.get("/payouts")
156
+ .summary("List my payouts")
157
+ .auth()
158
+ .handler(async ({ actor }) => {
159
+ const vendorId = requireVendorId(actor);
160
+ return services.payout.listPayouts({ vendorId });
161
+ });
162
+ // ─── My balance ────────────────────────────────────────────────────────────
163
+ r.get("/balance")
164
+ .summary("Get my balance")
165
+ .auth()
166
+ .handler(async ({ actor }) => {
167
+ const vendorId = requireVendorId(actor);
168
+ const balance = await services.payout.getBalance(vendorId);
169
+ const ledger = await services.payout.getLedger(vendorId);
170
+ return { balanceCents: balance, ledger };
171
+ });
172
+ // ─── My analytics ──────────────────────────────────────────────────────────
173
+ r.get("/analytics")
174
+ .summary("Get my analytics")
175
+ .auth()
176
+ .handler(async ({ actor }) => {
177
+ const vendorId = requireVendorId(actor);
178
+ const rating = await services.review.getAggregateRating(vendorId);
179
+ const balance = await services.payout.getBalance(vendorId);
180
+ return { rating, balanceCents: balance };
181
+ });
182
+ // ─── My reviews ────────────────────────────────────────────────────────────
183
+ r.get("/reviews")
184
+ .summary("List my reviews")
185
+ .auth()
186
+ .handler(async ({ actor }) => {
187
+ const vendorId = requireVendorId(actor);
188
+ return services.review.getForVendor(vendorId, true);
189
+ });
190
+ // ─── Respond to review ─────────────────────────────────────────────────────
191
+ r.post("/reviews/{id}/respond")
192
+ .summary("Respond to a review")
193
+ .auth()
194
+ .input(RespondToReviewBodySchema)
195
+ .handler(async ({ actor, params, input }) => {
196
+ const vendorId = requireVendorId(actor);
197
+ const body = input;
198
+ // IDOR prevention: verify the review belongs to this vendor
199
+ const vendorReviews = await services.review.getForVendor(vendorId, true);
200
+ const ownsReview = vendorReviews.some((rev) => rev.id === params.id);
201
+ if (!ownsReview)
202
+ throw new Error("Forbidden: review does not belong to this vendor");
203
+ const updated = await services.review.respond(params.id, body.response);
204
+ if (!updated)
205
+ throw new Error("Review not found");
206
+ return updated;
207
+ });
208
+ // ─── My returns ────────────────────────────────────────────────────────────
209
+ r.get("/returns")
210
+ .summary("List my returns")
211
+ .auth()
212
+ .handler(async ({ actor }) => {
213
+ const vendorId = requireVendorId(actor);
214
+ return services.return.listByVendor(vendorId);
215
+ });
216
+ // ─── Approve return ────────────────────────────────────────────────────────
217
+ r.post("/returns/{id}/approve")
218
+ .summary("Approve a return request")
219
+ .auth()
220
+ .input(ApproveReturnBodySchema)
221
+ .handler(async ({ actor, params, input }) => {
222
+ const vendorId = requireVendorId(actor);
223
+ const ret = await services.return.getById(params.id);
224
+ if (!ret)
225
+ throw new Error("Return not found");
226
+ const subOrder = await services.subOrder.getById(ret.subOrderId);
227
+ if (!subOrder || subOrder.vendorId !== vendorId)
228
+ throw new Error("Forbidden");
229
+ const body = input;
230
+ const updated = await services.return.vendorApprove(params.id, body.refundAmountCents);
231
+ if (!updated)
232
+ throw new Error("Return not found");
233
+ return updated;
234
+ });
235
+ // ─── Reject return ─────────────────────────────────────────────────────────
236
+ r.post("/returns/{id}/reject")
237
+ .summary("Reject a return request")
238
+ .auth()
239
+ .input(RejectReturnBodySchema)
240
+ .handler(async ({ actor, params, input }) => {
241
+ const vendorId = requireVendorId(actor);
242
+ const ret = await services.return.getById(params.id);
243
+ if (!ret)
244
+ throw new Error("Return not found");
245
+ const subOrder = await services.subOrder.getById(ret.subOrderId);
246
+ if (!subOrder || subOrder.vendorId !== vendorId)
247
+ throw new Error("Forbidden");
248
+ const body = input;
249
+ const updated = await services.return.vendorReject(params.id, body.notes);
250
+ if (!updated)
251
+ throw new Error("Return not found");
252
+ return updated;
253
+ });
254
+ return r.routes();
255
+ }
@@ -0,0 +1,11 @@
1
+ import type { PluginRouteRegistration } from "@unifiedcommerce/core";
2
+ import type { VendorService } from "../services/vendor";
3
+ import type { PayoutService } from "../services/payout";
4
+ import type { ReviewService } from "../services/review";
5
+ import type { MarketplacePluginOptions } from "../types";
6
+ export declare function buildVendorRoutes(services: {
7
+ vendor: VendorService;
8
+ payout: PayoutService;
9
+ review: ReviewService;
10
+ }, options?: MarketplacePluginOptions): PluginRouteRegistration[];
11
+ //# sourceMappingURL=vendors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vendors.d.ts","sourceRoot":"","sources":["../../src/routes/vendors.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAErE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AAUzD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE;IAC1C,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,aAAa,CAAC;CACvB,EAAE,OAAO,CAAC,EAAE,wBAAwB,GAAG,uBAAuB,EAAE,CAoLhE"}
@@ -0,0 +1,185 @@
1
+ import { router, resolveOrgId } from "@unifiedcommerce/core";
2
+ import { CreateVendorBodySchema, UpdateVendorBodySchema, RejectVendorBodySchema, SuspendVendorBodySchema, UploadDocumentBodySchema, } from "../schemas/vendors";
3
+ import { stripUndefined, stripVendorSecrets } from "./util";
4
+ export function buildVendorRoutes(services, options) {
5
+ const r = router("Marketplace - Vendors", "/marketplace/vendors");
6
+ // ─── List vendors ──────────────────────────────────────────────────────────
7
+ r.get("/")
8
+ .summary("List all vendors")
9
+ .permission("marketplace:admin")
10
+ .handler(async ({ query }) => {
11
+ const vendors = await services.vendor.list(stripUndefined({
12
+ status: query.status,
13
+ tier: query.tier,
14
+ search: query.search,
15
+ }));
16
+ return vendors.map(stripVendorSecrets);
17
+ });
18
+ // ─── Create vendor ─────────────────────────────────────────────────────────
19
+ r.post("/")
20
+ .summary("Create a vendor")
21
+ .permission("marketplace:admin")
22
+ .input(CreateVendorBodySchema)
23
+ .handler(async ({ input, actor }) => {
24
+ const body = input;
25
+ const defaultBps = options?.defaultCommissionRateBps ?? 1000;
26
+ return services.vendor.create(resolveOrgId(actor), stripUndefined({
27
+ ...body,
28
+ commissionRateBps: body.commissionRateBps ?? defaultBps,
29
+ }));
30
+ });
31
+ // ─── Get vendor detail ─────────────────────────────────────────────────────
32
+ r.get("/{id}")
33
+ .summary("Get vendor detail")
34
+ .permission("marketplace:admin")
35
+ .handler(async ({ params }) => {
36
+ const vendor = await services.vendor.getById(params.id);
37
+ if (!vendor)
38
+ throw new Error("Vendor not found");
39
+ return stripVendorSecrets(vendor);
40
+ });
41
+ // ─── Update vendor ─────────────────────────────────────────────────────────
42
+ r.patch("/{id}")
43
+ .summary("Update a vendor")
44
+ .permission("marketplace:admin")
45
+ .input(UpdateVendorBodySchema)
46
+ .handler(async ({ params, input }) => {
47
+ const body = input;
48
+ const updated = await services.vendor.update(params.id, stripUndefined(body));
49
+ if (!updated)
50
+ throw new Error("Vendor not found");
51
+ return stripVendorSecrets(updated);
52
+ });
53
+ // ─── Approve vendor ────────────────────────────────────────────────────────
54
+ r.post("/{id}/approve")
55
+ .summary("Approve a vendor application")
56
+ .permission("marketplace:admin")
57
+ .handler(async ({ params }) => {
58
+ const vendor = await services.vendor.getById(params.id);
59
+ if (!vendor)
60
+ throw new Error("Vendor not found");
61
+ const approved = await services.vendor.approve(params.id);
62
+ if (!approved)
63
+ throw new Error("Vendor not found");
64
+ return stripVendorSecrets(approved);
65
+ });
66
+ // ─── Reject vendor ─────────────────────────────────────────────────────────
67
+ r.post("/{id}/reject")
68
+ .summary("Reject a vendor application")
69
+ .permission("marketplace:admin")
70
+ .input(RejectVendorBodySchema)
71
+ .handler(async ({ params, input }) => {
72
+ const body = input;
73
+ const vendor = await services.vendor.getById(params.id);
74
+ if (!vendor)
75
+ throw new Error("Vendor not found");
76
+ const rejected = await services.vendor.reject(params.id, body.reason);
77
+ if (!rejected)
78
+ throw new Error("Vendor not found");
79
+ return stripVendorSecrets(rejected);
80
+ });
81
+ // ─── Suspend vendor ────────────────────────────────────────────────────────
82
+ r.post("/{id}/suspend")
83
+ .summary("Suspend a vendor")
84
+ .permission("marketplace:admin")
85
+ .input(SuspendVendorBodySchema)
86
+ .handler(async ({ params, input }) => {
87
+ const body = input;
88
+ const vendor = await services.vendor.getById(params.id);
89
+ if (!vendor)
90
+ throw new Error("Vendor not found");
91
+ const suspended = await services.vendor.suspend(params.id, body.reason);
92
+ if (!suspended)
93
+ throw new Error("Vendor not found");
94
+ return stripVendorSecrets(suspended);
95
+ });
96
+ // ─── Reinstate vendor ──────────────────────────────────────────────────────
97
+ r.post("/{id}/reinstate")
98
+ .summary("Reinstate a suspended vendor")
99
+ .permission("marketplace:admin")
100
+ .handler(async ({ params }) => {
101
+ const vendor = await services.vendor.getById(params.id);
102
+ if (!vendor)
103
+ throw new Error("Vendor not found");
104
+ const reinstated = await services.vendor.reinstate(params.id);
105
+ if (!reinstated)
106
+ throw new Error("Vendor not found");
107
+ return stripVendorSecrets(reinstated);
108
+ });
109
+ // ─── Upload vendor document ────────────────────────────────────────────────
110
+ r.post("/{id}/documents")
111
+ .summary("Upload a vendor document")
112
+ .permission("marketplace:admin")
113
+ .input(UploadDocumentBodySchema)
114
+ .handler(async ({ params, input }) => {
115
+ const body = input;
116
+ const vendor = await services.vendor.getById(params.id);
117
+ if (!vendor)
118
+ throw new Error("Vendor not found");
119
+ return services.vendor.uploadDocument(params.id, {
120
+ type: body.type,
121
+ fileUrl: body.fileUrl,
122
+ });
123
+ });
124
+ // ─── List vendor documents ─────────────────────────────────────────────────
125
+ r.get("/{id}/documents")
126
+ .summary("List vendor documents")
127
+ .permission("marketplace:admin")
128
+ .handler(async ({ params }) => {
129
+ const vendor = await services.vendor.getById(params.id);
130
+ if (!vendor)
131
+ throw new Error("Vendor not found");
132
+ return services.vendor.listDocuments(params.id);
133
+ });
134
+ // ─── Approve document ──────────────────────────────────────────────────────
135
+ r.post("/{id}/documents/{docId}/approve")
136
+ .summary("Approve a vendor document")
137
+ .permission("marketplace:admin")
138
+ .handler(async ({ params }) => {
139
+ const updated = await services.vendor.approveDocument(params.docId);
140
+ if (!updated)
141
+ throw new Error("Document not found");
142
+ return updated;
143
+ });
144
+ // ─── Reject document ───────────────────────────────────────────────────────
145
+ r.post("/{id}/documents/{docId}/reject")
146
+ .summary("Reject a vendor document")
147
+ .permission("marketplace:admin")
148
+ .handler(async ({ params }) => {
149
+ const updated = await services.vendor.rejectDocument(params.docId);
150
+ if (!updated)
151
+ throw new Error("Document not found");
152
+ return updated;
153
+ });
154
+ // ─── Vendor balance ────────────────────────────────────────────────────────
155
+ r.get("/{id}/balance")
156
+ .summary("Get vendor balance")
157
+ .permission("marketplace:admin")
158
+ .handler(async ({ params }) => {
159
+ const vendor = await services.vendor.getById(params.id);
160
+ if (!vendor)
161
+ throw new Error("Vendor not found");
162
+ const balance = await services.payout.getBalance(params.id);
163
+ const ledger = await services.payout.getLedger(params.id);
164
+ return { balanceCents: balance, ledger };
165
+ });
166
+ // ─── Vendor performance ────────────────────────────────────────────────────
167
+ r.get("/{id}/performance")
168
+ .summary("Get vendor performance metrics")
169
+ .permission("marketplace:admin")
170
+ .handler(async ({ params }) => {
171
+ const vendor = await services.vendor.getById(params.id);
172
+ if (!vendor)
173
+ throw new Error("Vendor not found");
174
+ const rating = await services.review.getAggregateRating(params.id);
175
+ const balance = await services.payout.getBalance(params.id);
176
+ return {
177
+ vendorId: params.id,
178
+ performanceScore: vendor.performanceScore,
179
+ tier: vendor.tier,
180
+ rating,
181
+ balanceCents: balance,
182
+ };
183
+ });
184
+ return r.routes();
185
+ }