@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,210 @@
1
+ import type { MCPTool } from "@unifiedcommerce/core";
2
+ import type { VendorService } from "./services/vendor";
3
+ import type { SubOrderService } from "./services/sub-order";
4
+ import type { CommissionService } from "./services/commission";
5
+ import type { PayoutService } from "./services/payout";
6
+ import type { DisputeService } from "./services/dispute";
7
+ import type { ReviewService } from "./services/review";
8
+ import type { RFQService } from "./services/rfq";
9
+ import type { MarketplacePluginOptions } from "./types";
10
+
11
+ interface MCPServices {
12
+ vendor: VendorService;
13
+ subOrder: SubOrderService;
14
+ commission: CommissionService;
15
+ payout: PayoutService;
16
+ dispute: DisputeService;
17
+ review: ReviewService;
18
+ rfq?: RFQService | undefined;
19
+ }
20
+
21
+ function p(params: unknown): Record<string, unknown> {
22
+ return (params ?? {}) as Record<string, unknown>;
23
+ }
24
+
25
+ export function buildMCPTools(services: MCPServices, _options: MarketplacePluginOptions): MCPTool[] {
26
+ const tools: MCPTool[] = [
27
+ {
28
+ name: "marketplace_vendor_list",
29
+ description: "List marketplace vendors with optional status/tier filters.",
30
+ inputSchema: {
31
+ type: "object",
32
+ properties: {
33
+ status: { type: "string" },
34
+ tier: { type: "string" },
35
+ search: { type: "string" },
36
+ },
37
+ },
38
+ async handler(params: unknown) {
39
+ const rows = await services.vendor.list(p(params) as { status?: string; tier?: string; search?: string });
40
+ return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] };
41
+ },
42
+ },
43
+ {
44
+ name: "marketplace_vendor_performance",
45
+ description: "Get vendor performance metrics including rating, tier, and score.",
46
+ inputSchema: {
47
+ type: "object",
48
+ required: ["vendorId"],
49
+ properties: { vendorId: { type: "string" } },
50
+ },
51
+ async handler(params: unknown) {
52
+ const { vendorId } = p(params) as { vendorId: string };
53
+ const vendor = await services.vendor.getById(vendorId);
54
+ if (!vendor) return { error: { code: "NOT_FOUND", message: "Vendor not found." } };
55
+
56
+ const rating = await services.review.getAggregateRating(vendorId);
57
+ return {
58
+ content: [{
59
+ type: "text",
60
+ text: JSON.stringify({
61
+ vendorId: vendor.id,
62
+ name: vendor.name,
63
+ tier: vendor.tier,
64
+ performanceScore: vendor.performanceScore,
65
+ rating,
66
+ }, null, 2),
67
+ }],
68
+ };
69
+ },
70
+ },
71
+ {
72
+ name: "marketplace_vendor_balance",
73
+ description: "Get vendor current balance and recent ledger entries.",
74
+ inputSchema: {
75
+ type: "object",
76
+ required: ["vendorId"],
77
+ properties: { vendorId: { type: "string" } },
78
+ },
79
+ async handler(params: unknown) {
80
+ const { vendorId } = p(params) as { vendorId: string };
81
+ const balance = await services.payout.getBalance(vendorId);
82
+ const ledger = await services.payout.getLedger(vendorId, 20);
83
+ return {
84
+ content: [{
85
+ type: "text",
86
+ text: JSON.stringify({ vendorId, balance, recentEntries: ledger }, null, 2),
87
+ }],
88
+ };
89
+ },
90
+ },
91
+ {
92
+ name: "marketplace_suborder_update",
93
+ description: "Transition a sub-order status (confirm, ship, deliver, cancel).",
94
+ inputSchema: {
95
+ type: "object",
96
+ required: ["subOrderId", "action"],
97
+ properties: {
98
+ subOrderId: { type: "string" },
99
+ action: { type: "string", enum: ["confirm", "ship", "deliver", "cancel"] },
100
+ trackingNumber: { type: "string" },
101
+ carrier: { type: "string" },
102
+ reason: { type: "string" },
103
+ },
104
+ },
105
+ async handler(params: unknown) {
106
+ const args = p(params) as {
107
+ subOrderId: string; action: string;
108
+ trackingNumber?: string; carrier?: string; reason?: string;
109
+ };
110
+ try {
111
+ let result;
112
+ switch (args.action) {
113
+ case "confirm":
114
+ result = await services.subOrder.confirm(args.subOrderId);
115
+ break;
116
+ case "ship":
117
+ result = await services.subOrder.ship(args.subOrderId, {
118
+ trackingNumber: args.trackingNumber ?? "",
119
+ carrier: args.carrier ?? "",
120
+ });
121
+ break;
122
+ case "deliver":
123
+ result = await services.subOrder.deliver(args.subOrderId);
124
+ break;
125
+ case "cancel":
126
+ result = await services.subOrder.cancel(args.subOrderId, args.reason);
127
+ break;
128
+ default:
129
+ return { error: { code: "INVALID_ACTION", message: `Unknown action: ${args.action}` } };
130
+ }
131
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
132
+ } catch (err: unknown) {
133
+ return { error: { code: "TRANSITION_ERROR", message: err instanceof Error ? err.message : "Unknown error" } };
134
+ }
135
+ },
136
+ },
137
+ {
138
+ name: "marketplace_dispute_summary",
139
+ description: "List open disputes with deadlines.",
140
+ inputSchema: { type: "object" },
141
+ async handler() {
142
+ const open = await services.dispute.list({ status: "open" });
143
+ const pending = await services.dispute.list({ status: "vendor_response_pending" });
144
+ const escalated = await services.dispute.list({ status: "escalated" });
145
+ return {
146
+ content: [{
147
+ type: "text",
148
+ text: JSON.stringify({ open, vendorResponsePending: pending, escalated }, null, 2),
149
+ }],
150
+ };
151
+ },
152
+ },
153
+ {
154
+ name: "marketplace_payout_run",
155
+ description: "Trigger a payout cycle for all eligible vendors.",
156
+ inputSchema: { type: "object" },
157
+ async handler() {
158
+ const results = await services.payout.runPayoutCycle();
159
+ return {
160
+ content: [{
161
+ type: "text",
162
+ text: JSON.stringify({ payoutsProcessed: results.length, details: results }, null, 2),
163
+ }],
164
+ };
165
+ },
166
+ },
167
+ {
168
+ name: "marketplace_commission_preview",
169
+ description: "Preview effective commission rate for a vendor+category+amount.",
170
+ inputSchema: {
171
+ type: "object",
172
+ required: ["vendorId"],
173
+ properties: {
174
+ vendorId: { type: "string" },
175
+ categorySlug: { type: "string" },
176
+ volumeCents: { type: "number" },
177
+ },
178
+ },
179
+ async handler(params: unknown) {
180
+ const { vendorId, categorySlug, volumeCents } = p(params) as {
181
+ vendorId: string; categorySlug?: string; volumeCents?: number;
182
+ };
183
+ const preview = await services.commission.previewRate(vendorId, categorySlug, volumeCents);
184
+ return { content: [{ type: "text", text: JSON.stringify(preview, null, 2) }] };
185
+ },
186
+ },
187
+ ];
188
+
189
+ // B2B: RFQ tool
190
+ if (services.rfq) {
191
+ const rfqService = services.rfq;
192
+ tools.push({
193
+ name: "marketplace_rfq_list",
194
+ description: "List open RFQs with optional category filter.",
195
+ inputSchema: {
196
+ type: "object",
197
+ properties: {
198
+ status: { type: "string" },
199
+ categorySlug: { type: "string" },
200
+ },
201
+ },
202
+ async handler(params: unknown) {
203
+ const rows = await rfqService.list(p(params) as { status?: string; categorySlug?: string });
204
+ return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] };
205
+ },
206
+ });
207
+ }
208
+
209
+ return tools;
210
+ }
@@ -0,0 +1,179 @@
1
+ import { router } from "@unifiedcommerce/core";
2
+ import type { PluginRouteRegistration } from "@unifiedcommerce/core";
3
+ import type { z } from "@hono/zod-openapi";
4
+ import type { RFQService } from "../services/rfq";
5
+ import type { ContractPriceService } from "../services/contract-price";
6
+ import type { MarketplacePluginOptions } from "../types";
7
+ import {
8
+ CreateRFQBodySchema,
9
+ RespondRFQBodySchema,
10
+ AwardRFQBodySchema,
11
+ CreateContractPriceBodySchema,
12
+ UpdateContractPriceBodySchema,
13
+ } from "../schemas/b2b";
14
+ import { stripUndefined } from "./util";
15
+
16
+ export function buildB2BRoutes(services: {
17
+ rfq?: RFQService | undefined;
18
+ contractPrice?: ContractPriceService | undefined;
19
+ }, options: MarketplacePluginOptions): PluginRouteRegistration[] {
20
+ const allRoutes: PluginRouteRegistration[] = [];
21
+
22
+ // ═══════════════════════════════════════════════════════════════════════════
23
+ // RFQ (Request for Quote)
24
+ // ═══════════════════════════════════════════════════════════════════════════
25
+
26
+ if (services.rfq) {
27
+ const rfqSvc = services.rfq;
28
+ const rfq = router("Marketplace - B2B", "/marketplace/rfq");
29
+
30
+ rfq.post("/")
31
+ .summary("Create a Request for Quote")
32
+ .auth()
33
+ .input(CreateRFQBodySchema)
34
+ .handler(async ({ input }) => {
35
+ const body = input as z.infer<typeof CreateRFQBodySchema>;
36
+ return rfqSvc.create(stripUndefined({
37
+ buyerId: body.buyerId,
38
+ title: body.title,
39
+ description: body.description,
40
+ categorySlug: body.categorySlug,
41
+ quantity: body.quantity,
42
+ budgetCents: body.budgetCents,
43
+ currency: body.currency,
44
+ deadlineAt: body.deadlineAt ? new Date(body.deadlineAt) : undefined,
45
+ metadata: body.metadata,
46
+ }));
47
+ });
48
+
49
+ rfq.get("/")
50
+ .summary("List Requests for Quote")
51
+ .auth()
52
+ .handler(async ({ query }) => {
53
+ return rfqSvc.list(stripUndefined({
54
+ status: query.status as string | undefined,
55
+ categorySlug: query.categorySlug as string | undefined,
56
+ }));
57
+ });
58
+
59
+ rfq.get("/{id}")
60
+ .summary("Get RFQ detail")
61
+ .auth()
62
+ .handler(async ({ params }) => {
63
+ const item = await rfqSvc.getById(params.id!);
64
+ if (!item) throw new Error("RFQ not found");
65
+ const responses = await rfqSvc.getResponses(item.id);
66
+ return { ...item, responses };
67
+ });
68
+
69
+ rfq.post("/{id}/respond")
70
+ .summary("Submit a vendor response to an RFQ")
71
+ .auth()
72
+ .input(RespondRFQBodySchema)
73
+ .handler(async ({ params, input }) => {
74
+ const body = input as z.infer<typeof RespondRFQBodySchema>;
75
+ const item = await rfqSvc.getById(params.id!);
76
+ if (!item) throw new Error("RFQ not found");
77
+ return rfqSvc.respond(item.id, stripUndefined({
78
+ vendorId: body.vendorId,
79
+ unitPriceCents: body.unitPriceCents,
80
+ totalPriceCents: body.totalPriceCents,
81
+ leadTimeDays: body.leadTimeDays,
82
+ notes: body.notes,
83
+ }));
84
+ });
85
+
86
+ rfq.post("/{id}/award")
87
+ .summary("Award an RFQ to a vendor")
88
+ .auth()
89
+ .permission("marketplace:admin")
90
+ .input(AwardRFQBodySchema)
91
+ .handler(async ({ params, input }) => {
92
+ const body = input as z.infer<typeof AwardRFQBodySchema>;
93
+ const updated = await rfqSvc.award(params.id!, body.vendorId);
94
+ if (!updated) throw new Error("RFQ not found");
95
+ return updated;
96
+ });
97
+
98
+ rfq.post("/{id}/close")
99
+ .summary("Close an RFQ")
100
+ .auth()
101
+ .permission("marketplace:admin")
102
+ .handler(async ({ params }) => {
103
+ const updated = await rfqSvc.close(params.id!);
104
+ if (!updated) throw new Error("RFQ not found");
105
+ return updated;
106
+ });
107
+
108
+ allRoutes.push(...rfq.routes());
109
+ }
110
+
111
+ // ═══════════════════════════════════════════════════════════════════════════
112
+ // CONTRACT PRICES
113
+ // ═══════════════════════════════════════════════════════════════════════════
114
+
115
+ if (services.contractPrice) {
116
+ const cpSvc = services.contractPrice;
117
+ const cp = router("Marketplace - B2B", "/marketplace/contract-prices");
118
+
119
+ cp.get("/")
120
+ .summary("List contract prices")
121
+ .auth()
122
+ .handler(async ({ query }) => {
123
+ return cpSvc.list(stripUndefined({
124
+ vendorId: query.vendorId as string | undefined,
125
+ buyerId: query.buyerId as string | undefined,
126
+ }));
127
+ });
128
+
129
+ cp.post("/")
130
+ .summary("Create a contract price")
131
+ .auth()
132
+ .permission("marketplace:admin")
133
+ .input(CreateContractPriceBodySchema)
134
+ .handler(async ({ input }) => {
135
+ const body = input as z.infer<typeof CreateContractPriceBodySchema>;
136
+ return cpSvc.create(stripUndefined({
137
+ vendorId: body.vendorId,
138
+ buyerId: body.buyerId,
139
+ entityId: body.entityId,
140
+ variantId: body.variantId,
141
+ priceCents: body.priceCents,
142
+ minQuantity: body.minQuantity,
143
+ currency: body.currency,
144
+ validFrom: body.validFrom ? new Date(body.validFrom) : undefined,
145
+ validUntil: body.validUntil ? new Date(body.validUntil) : undefined,
146
+ }));
147
+ });
148
+
149
+ cp.patch("/{id}")
150
+ .summary("Update a contract price")
151
+ .auth()
152
+ .permission("marketplace:admin")
153
+ .input(UpdateContractPriceBodySchema)
154
+ .handler(async ({ params, input }) => {
155
+ const body = input as z.infer<typeof UpdateContractPriceBodySchema>;
156
+ const updateData: Record<string, unknown> = {};
157
+ if (body.priceCents !== undefined) updateData.priceCents = body.priceCents;
158
+ if (body.minQuantity !== undefined) updateData.minQuantity = body.minQuantity;
159
+ if (body.validFrom !== undefined) updateData.validFrom = body.validFrom ? new Date(body.validFrom) : null;
160
+ if (body.validUntil !== undefined) updateData.validUntil = body.validUntil ? new Date(body.validUntil) : null;
161
+ const updated = await cpSvc.update(params.id!, updateData);
162
+ if (!updated) throw new Error("Contract price not found");
163
+ return updated;
164
+ });
165
+
166
+ cp.delete("/{id}")
167
+ .summary("Delete a contract price")
168
+ .auth()
169
+ .permission("marketplace:admin")
170
+ .handler(async ({ params }) => {
171
+ await cpSvc.delete(params.id!);
172
+ return { deleted: true };
173
+ });
174
+
175
+ allRoutes.push(...cp.routes());
176
+ }
177
+
178
+ return allRoutes;
179
+ }
@@ -0,0 +1,95 @@
1
+ import { router } from "@unifiedcommerce/core";
2
+ import type { PluginRouteRegistration } from "@unifiedcommerce/core";
3
+ import type { z } from "@hono/zod-openapi";
4
+ import type { CommissionService } from "../services/commission";
5
+ import {
6
+ CreateCommissionRuleBodySchema,
7
+ UpdateCommissionRuleBodySchema,
8
+ PreviewCommissionBodySchema,
9
+ } from "../schemas/commission";
10
+ import { stripUndefined } from "./util";
11
+
12
+ export function buildCommissionRoutes(services: {
13
+ commission: CommissionService;
14
+ }): PluginRouteRegistration[] {
15
+ const r = router("Marketplace - Commission", "/marketplace/commission-rules");
16
+
17
+ // ─── List commission rules ─────────────────────────────────────────────────
18
+ r.get("/")
19
+ .summary("List all commission rules")
20
+ .permission("marketplace:admin")
21
+ .handler(async () => {
22
+ return services.commission.listRules();
23
+ });
24
+
25
+ // ─── Create commission rule ────────────────────────────────────────────────
26
+ r.post("/")
27
+ .summary("Create a commission rule")
28
+ .permission("marketplace:admin")
29
+ .input(CreateCommissionRuleBodySchema)
30
+ .handler(async ({ input }) => {
31
+ const body = input as z.infer<typeof CreateCommissionRuleBodySchema>;
32
+ return services.commission.createRule(stripUndefined({
33
+ name: body.name,
34
+ type: body.type,
35
+ rateBps: body.rateBps,
36
+ categorySlug: body.categorySlug,
37
+ vendorId: body.vendorId,
38
+ vendorTier: body.vendorTier,
39
+ minVolumeCents: body.minVolumeCents,
40
+ maxVolumeCents: body.maxVolumeCents,
41
+ validFrom: body.validFrom ? new Date(body.validFrom) : undefined,
42
+ validUntil: body.validUntil ? new Date(body.validUntil) : undefined,
43
+ priority: body.priority,
44
+ }));
45
+ });
46
+
47
+ // ─── Update commission rule ────────────────────────────────────────────────
48
+ r.patch("/{id}")
49
+ .summary("Update a commission rule")
50
+ .permission("marketplace:admin")
51
+ .input(UpdateCommissionRuleBodySchema)
52
+ .handler(async ({ params, input }) => {
53
+ const body = input as z.infer<typeof UpdateCommissionRuleBodySchema>;
54
+ const updateData: Record<string, unknown> = {};
55
+ if (body.name !== undefined) updateData.name = body.name;
56
+ if (body.rateBps !== undefined) updateData.rateBps = body.rateBps;
57
+ if (body.categorySlug !== undefined) updateData.categorySlug = body.categorySlug;
58
+ if (body.vendorTier !== undefined) updateData.vendorTier = body.vendorTier;
59
+ if (body.minVolumeCents !== undefined) updateData.minVolumeCents = body.minVolumeCents;
60
+ if (body.maxVolumeCents !== undefined) updateData.maxVolumeCents = body.maxVolumeCents;
61
+ if (body.validFrom !== undefined) updateData.validFrom = body.validFrom ? new Date(body.validFrom) : null;
62
+ if (body.validUntil !== undefined) updateData.validUntil = body.validUntil ? new Date(body.validUntil) : null;
63
+ if (body.priority !== undefined) updateData.priority = body.priority;
64
+ if (body.isActive !== undefined) updateData.isActive = body.isActive;
65
+
66
+ const updated = await services.commission.updateRule(params.id!, updateData);
67
+ if (!updated) throw new Error("Commission rule not found");
68
+ return updated;
69
+ });
70
+
71
+ // ─── Delete commission rule ────────────────────────────────────────────────
72
+ r.delete("/{id}")
73
+ .summary("Delete a commission rule")
74
+ .permission("marketplace:admin")
75
+ .handler(async ({ params }) => {
76
+ await services.commission.deleteRule(params.id!);
77
+ return { deleted: true };
78
+ });
79
+
80
+ // ─── Preview commission rate ───────────────────────────────────────────────
81
+ r.post("/preview")
82
+ .summary("Preview the commission rate for a vendor")
83
+ .permission("marketplace:admin")
84
+ .input(PreviewCommissionBodySchema)
85
+ .handler(async ({ input }) => {
86
+ const body = input as z.infer<typeof PreviewCommissionBodySchema>;
87
+ return services.commission.previewRate(
88
+ body.vendorId,
89
+ body.categorySlug,
90
+ body.volumeCents,
91
+ );
92
+ });
93
+
94
+ return r.routes();
95
+ }
@@ -0,0 +1,209 @@
1
+ import { router } from "@unifiedcommerce/core";
2
+ import type { PluginRouteRegistration } from "@unifiedcommerce/core";
3
+ import type { z } from "@hono/zod-openapi";
4
+ import type { DisputeService } from "../services/dispute";
5
+ import type { ReturnService } from "../services/return";
6
+ import type { ReviewService } from "../services/review";
7
+ import type { DisputeResolution, ReviewStatus } from "../types";
8
+ import {
9
+ OpenDisputeBodySchema,
10
+ RespondDisputeBodySchema,
11
+ ResolveDisputeBodySchema,
12
+ RequestReturnBodySchema,
13
+ ShipBackReturnBodySchema,
14
+ CreateReviewBodySchema,
15
+ ModerateReviewBodySchema,
16
+ } from "../schemas/disputes-returns-reviews";
17
+ import { stripUndefined } from "./util";
18
+
19
+ export function buildDisputesReturnsReviewsRoutes(services: {
20
+ dispute: DisputeService;
21
+ return: ReturnService;
22
+ review: ReviewService;
23
+ }): PluginRouteRegistration[] {
24
+ // ═══════════════════════════════════════════════════════════════════════════
25
+ // DISPUTES
26
+ // ═══════════════════════════════════════════════════════════════════════════
27
+ const disputes = router("Marketplace - Disputes", "/marketplace/disputes");
28
+
29
+ disputes.post("/")
30
+ .summary("Open a dispute")
31
+ .auth()
32
+ .input(OpenDisputeBodySchema)
33
+ .handler(async ({ input }) => {
34
+ const body = input as z.infer<typeof OpenDisputeBodySchema>;
35
+ return services.dispute.open(stripUndefined({
36
+ subOrderId: body.subOrderId,
37
+ openedBy: body.openedBy,
38
+ reason: body.reason,
39
+ description: body.description,
40
+ }));
41
+ });
42
+
43
+ disputes.get("/")
44
+ .summary("List disputes")
45
+ .auth()
46
+ .handler(async ({ query }) => {
47
+ return services.dispute.list(stripUndefined({
48
+ status: query.status as string | undefined,
49
+ subOrderId: query.subOrderId as string | undefined,
50
+ }));
51
+ });
52
+
53
+ disputes.get("/{id}")
54
+ .summary("Get dispute by ID")
55
+ .auth()
56
+ .handler(async ({ params }) => {
57
+ const dispute = await services.dispute.getById(params.id!);
58
+ if (!dispute) throw new Error("Dispute not found");
59
+ return dispute;
60
+ });
61
+
62
+ disputes.post("/{id}/respond")
63
+ .summary("Respond to a dispute")
64
+ .auth()
65
+ .input(RespondDisputeBodySchema)
66
+ .handler(async ({ params, input }) => {
67
+ const body = input as z.infer<typeof RespondDisputeBodySchema>;
68
+ return services.dispute.respond(params.id!, stripUndefined({
69
+ party: body.party,
70
+ note: body.note,
71
+ url: body.url,
72
+ }));
73
+ });
74
+
75
+ disputes.post("/{id}/escalate")
76
+ .summary("Escalate a dispute")
77
+ .auth()
78
+ .permission("marketplace:admin")
79
+ .handler(async ({ params }) => {
80
+ const updated = await services.dispute.escalate(params.id!);
81
+ if (!updated) throw new Error("Dispute not found");
82
+ return updated;
83
+ });
84
+
85
+ disputes.post("/{id}/resolve")
86
+ .summary("Resolve a dispute")
87
+ .auth()
88
+ .permission("marketplace:admin")
89
+ .input(ResolveDisputeBodySchema)
90
+ .handler(async ({ params, input }) => {
91
+ const body = input as z.infer<typeof ResolveDisputeBodySchema>;
92
+ const updated = await services.dispute.resolve(params.id!, stripUndefined({
93
+ resolution: body.resolution as DisputeResolution,
94
+ notes: body.notes,
95
+ refundAmountCents: body.refundAmountCents,
96
+ resolvedBy: body.resolvedBy,
97
+ }));
98
+ if (!updated) throw new Error("Dispute not found");
99
+ return updated;
100
+ });
101
+
102
+ // ═══════════════════════════════════════════════════════════════════════════
103
+ // RETURNS
104
+ // ═══════════════════════════════════════════════════════════════════════════
105
+ const returns = router("Marketplace - Returns", "/marketplace/returns");
106
+
107
+ returns.post("/")
108
+ .summary("Request a return")
109
+ .auth()
110
+ .input(RequestReturnBodySchema)
111
+ .handler(async ({ input }) => {
112
+ const body = input as z.infer<typeof RequestReturnBodySchema>;
113
+ return services.return.request(stripUndefined({
114
+ subOrderId: body.subOrderId,
115
+ customerId: body.customerId,
116
+ reason: body.reason,
117
+ description: body.description,
118
+ lineItems: body.lineItems as { entityId: string; quantity: number; reason?: string }[] | undefined,
119
+ }));
120
+ });
121
+
122
+ returns.get("/")
123
+ .summary("List returns")
124
+ .auth()
125
+ .handler(async ({ query }) => {
126
+ return services.return.list(stripUndefined({
127
+ subOrderId: query.subOrderId as string | undefined,
128
+ status: query.status as string | undefined,
129
+ }));
130
+ });
131
+
132
+ returns.get("/{id}")
133
+ .summary("Get return by ID")
134
+ .auth()
135
+ .handler(async ({ params }) => {
136
+ const ret = await services.return.getById(params.id!);
137
+ if (!ret) throw new Error("Return not found");
138
+ return ret;
139
+ });
140
+
141
+ returns.post("/{id}/ship-back")
142
+ .summary("Ship back a return")
143
+ .auth()
144
+ .input(ShipBackReturnBodySchema)
145
+ .handler(async ({ params, input }) => {
146
+ const body = input as z.infer<typeof ShipBackReturnBodySchema>;
147
+ const updated = await services.return.shipBack(params.id!, body.trackingNumber);
148
+ if (!updated) throw new Error("Return not found");
149
+ return updated;
150
+ });
151
+
152
+ returns.post("/{id}/receive")
153
+ .summary("Mark a return as received")
154
+ .auth()
155
+ .handler(async ({ params }) => {
156
+ const updated = await services.return.receive(params.id!);
157
+ if (!updated) throw new Error("Return not found");
158
+ return updated;
159
+ });
160
+
161
+ // ═══════════════════════════════════════════════════════════════════════════
162
+ // REVIEWS
163
+ // ═══════════════════════════════════════════════════════════════════════════
164
+ const vendorReviews = router("Marketplace - Reviews", "/marketplace/vendors");
165
+
166
+ vendorReviews.post("/{id}/reviews")
167
+ .summary("Create a vendor review")
168
+ .auth()
169
+ .input(CreateReviewBodySchema)
170
+ .handler(async ({ params, input }) => {
171
+ const body = input as z.infer<typeof CreateReviewBodySchema>;
172
+ return services.review.create(stripUndefined({
173
+ vendorId: params.id!,
174
+ customerId: body.customerId,
175
+ orderId: body.orderId,
176
+ rating: body.rating,
177
+ title: body.title,
178
+ body: body.body,
179
+ }));
180
+ });
181
+
182
+ vendorReviews.get("/{id}/reviews")
183
+ .summary("List reviews for a vendor")
184
+ .auth()
185
+ .handler(async ({ params }) => {
186
+ return services.review.getForVendor(params.id!);
187
+ });
188
+
189
+ const reviewsMod = router("Marketplace - Reviews", "/marketplace/reviews");
190
+
191
+ reviewsMod.patch("/{id}")
192
+ .summary("Moderate a review (update status)")
193
+ .auth()
194
+ .permission("marketplace:admin")
195
+ .input(ModerateReviewBodySchema)
196
+ .handler(async ({ params, input }) => {
197
+ const body = input as z.infer<typeof ModerateReviewBodySchema>;
198
+ const updated = await services.review.moderate(params.id!, body.status as ReviewStatus);
199
+ if (!updated) throw new Error("Review not found");
200
+ return updated;
201
+ });
202
+
203
+ return [
204
+ ...disputes.routes(),
205
+ ...returns.routes(),
206
+ ...vendorReviews.routes(),
207
+ ...reviewsMod.routes(),
208
+ ];
209
+ }