@porulle/plugin-marketplace 0.1.0

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 (118) 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 +97 -0
  11. package/dist/routes/b2b.d.ts +9 -0
  12. package/dist/routes/b2b.d.ts.map +1 -0
  13. package/dist/routes/b2b.js +156 -0
  14. package/dist/routes/commission.d.ts +6 -0
  15. package/dist/routes/commission.d.ts.map +1 -0
  16. package/dist/routes/commission.js +85 -0
  17. package/dist/routes/disputes-returns-reviews.d.ts +10 -0
  18. package/dist/routes/disputes-returns-reviews.d.ts.map +1 -0
  19. package/dist/routes/disputes-returns-reviews.js +179 -0
  20. package/dist/routes/payouts.d.ts +6 -0
  21. package/dist/routes/payouts.d.ts.map +1 -0
  22. package/dist/routes/payouts.js +40 -0
  23. package/dist/routes/sub-orders.d.ts +6 -0
  24. package/dist/routes/sub-orders.d.ts.map +1 -0
  25. package/dist/routes/sub-orders.js +44 -0
  26. package/dist/routes/util.d.ts +23 -0
  27. package/dist/routes/util.d.ts.map +1 -0
  28. package/dist/routes/util.js +41 -0
  29. package/dist/routes/vendor-portal.d.ts +14 -0
  30. package/dist/routes/vendor-portal.d.ts.map +1 -0
  31. package/dist/routes/vendor-portal.js +255 -0
  32. package/dist/routes/vendors.d.ts +11 -0
  33. package/dist/routes/vendors.d.ts.map +1 -0
  34. package/dist/routes/vendors.js +185 -0
  35. package/dist/schema.d.ts +3255 -0
  36. package/dist/schema.d.ts.map +1 -0
  37. package/dist/schema.js +225 -0
  38. package/dist/schemas/b2b.d.ts +1009 -0
  39. package/dist/schemas/b2b.d.ts.map +1 -0
  40. package/dist/schemas/b2b.js +208 -0
  41. package/dist/schemas/commission.d.ts +532 -0
  42. package/dist/schemas/commission.d.ts.map +1 -0
  43. package/dist/schemas/commission.js +113 -0
  44. package/dist/schemas/disputes-returns-reviews.d.ts +1405 -0
  45. package/dist/schemas/disputes-returns-reviews.d.ts.map +1 -0
  46. package/dist/schemas/disputes-returns-reviews.js +270 -0
  47. package/dist/schemas/payouts.d.ts +375 -0
  48. package/dist/schemas/payouts.d.ts.map +1 -0
  49. package/dist/schemas/payouts.js +78 -0
  50. package/dist/schemas/sub-orders.d.ts +303 -0
  51. package/dist/schemas/sub-orders.d.ts.map +1 -0
  52. package/dist/schemas/sub-orders.js +67 -0
  53. package/dist/schemas/vendor-portal.d.ts +1785 -0
  54. package/dist/schemas/vendor-portal.d.ts.map +1 -0
  55. package/dist/schemas/vendor-portal.js +294 -0
  56. package/dist/schemas/vendors.d.ts +1348 -0
  57. package/dist/schemas/vendors.d.ts.map +1 -0
  58. package/dist/schemas/vendors.js +245 -0
  59. package/dist/services/commission.d.ts +81 -0
  60. package/dist/services/commission.d.ts.map +1 -0
  61. package/dist/services/commission.js +98 -0
  62. package/dist/services/contract-price.d.ts +64 -0
  63. package/dist/services/contract-price.d.ts.map +1 -0
  64. package/dist/services/contract-price.js +57 -0
  65. package/dist/services/dispute.d.ts +156 -0
  66. package/dist/services/dispute.d.ts.map +1 -0
  67. package/dist/services/dispute.js +77 -0
  68. package/dist/services/payout.d.ts +126 -0
  69. package/dist/services/payout.d.ts.map +1 -0
  70. package/dist/services/payout.js +130 -0
  71. package/dist/services/return.d.ts +181 -0
  72. package/dist/services/return.d.ts.map +1 -0
  73. package/dist/services/return.js +80 -0
  74. package/dist/services/review.d.ts +70 -0
  75. package/dist/services/review.d.ts.map +1 -0
  76. package/dist/services/review.js +60 -0
  77. package/dist/services/rfq.d.ts +122 -0
  78. package/dist/services/rfq.d.ts.map +1 -0
  79. package/dist/services/rfq.js +60 -0
  80. package/dist/services/sub-order.d.ts +336 -0
  81. package/dist/services/sub-order.d.ts.map +1 -0
  82. package/dist/services/sub-order.js +121 -0
  83. package/dist/services/vendor.d.ts +528 -0
  84. package/dist/services/vendor.d.ts.map +1 -0
  85. package/dist/services/vendor.js +119 -0
  86. package/dist/types.d.ts +67 -0
  87. package/dist/types.d.ts.map +1 -0
  88. package/dist/types.js +13 -0
  89. package/package.json +58 -0
  90. package/src/analytics-models.ts +75 -0
  91. package/src/hooks.ts +215 -0
  92. package/src/index.ts +116 -0
  93. package/src/routes/b2b.ts +179 -0
  94. package/src/routes/commission.ts +95 -0
  95. package/src/routes/disputes-returns-reviews.ts +209 -0
  96. package/src/routes/payouts.ts +49 -0
  97. package/src/routes/sub-orders.ts +54 -0
  98. package/src/routes/util.ts +42 -0
  99. package/src/routes/vendor-portal.ts +277 -0
  100. package/src/routes/vendors.ts +201 -0
  101. package/src/schema.ts +260 -0
  102. package/src/schemas/b2b.ts +238 -0
  103. package/src/schemas/commission.ts +129 -0
  104. package/src/schemas/disputes-returns-reviews.ts +311 -0
  105. package/src/schemas/payouts.ts +90 -0
  106. package/src/schemas/sub-orders.ts +77 -0
  107. package/src/schemas/vendor-portal.ts +344 -0
  108. package/src/schemas/vendors.ts +281 -0
  109. package/src/services/commission.ts +120 -0
  110. package/src/services/contract-price.ts +80 -0
  111. package/src/services/dispute.ts +92 -0
  112. package/src/services/payout.ts +154 -0
  113. package/src/services/return.ts +92 -0
  114. package/src/services/review.ts +76 -0
  115. package/src/services/rfq.ts +82 -0
  116. package/src/services/sub-order.ts +136 -0
  117. package/src/services/vendor.ts +151 -0
  118. package/src/types.ts +164 -0
@@ -0,0 +1,179 @@
1
+ import { router } from "@porulle/core";
2
+ import type { PluginRouteRegistration } from "@porulle/core";
3
+ import type { z } from "@hono/zod-openapi";
4
+ import type { RFQService } from "../services/rfq.js";
5
+ import type { ContractPriceService } from "../services/contract-price.js";
6
+ import type { MarketplacePluginOptions } from "../types.js";
7
+ import {
8
+ CreateRFQBodySchema,
9
+ RespondRFQBodySchema,
10
+ AwardRFQBodySchema,
11
+ CreateContractPriceBodySchema,
12
+ UpdateContractPriceBodySchema,
13
+ } from "../schemas/b2b.js";
14
+ import { stripUndefined } from "./util.js";
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 "@porulle/core";
2
+ import type { PluginRouteRegistration } from "@porulle/core";
3
+ import type { z } from "@hono/zod-openapi";
4
+ import type { CommissionService } from "../services/commission.js";
5
+ import {
6
+ CreateCommissionRuleBodySchema,
7
+ UpdateCommissionRuleBodySchema,
8
+ PreviewCommissionBodySchema,
9
+ } from "../schemas/commission.js";
10
+ import { stripUndefined } from "./util.js";
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 "@porulle/core";
2
+ import type { PluginRouteRegistration } from "@porulle/core";
3
+ import type { z } from "@hono/zod-openapi";
4
+ import type { DisputeService } from "../services/dispute.js";
5
+ import type { ReturnService } from "../services/return.js";
6
+ import type { ReviewService } from "../services/review.js";
7
+ import type { DisputeResolution, ReviewStatus } from "../types.js";
8
+ import {
9
+ OpenDisputeBodySchema,
10
+ RespondDisputeBodySchema,
11
+ ResolveDisputeBodySchema,
12
+ RequestReturnBodySchema,
13
+ ShipBackReturnBodySchema,
14
+ CreateReviewBodySchema,
15
+ ModerateReviewBodySchema,
16
+ } from "../schemas/disputes-returns-reviews.js";
17
+ import { stripUndefined } from "./util.js";
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
+ }
@@ -0,0 +1,49 @@
1
+ import { router } from "@porulle/core";
2
+ import type { PluginRouteRegistration } from "@porulle/core";
3
+ import type { PayoutService } from "../services/payout.js";
4
+ import { stripUndefined } from "./util.js";
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 "@porulle/core";
2
+ import type { PluginRouteRegistration } from "@porulle/core";
3
+ import type { z } from "@hono/zod-openapi";
4
+ import type { SubOrderService } from "../services/sub-order.js";
5
+ import type { SubOrderStatus } from "../types.js";
6
+ import { UpdateSubOrderStatusBodySchema } from "../schemas/sub-orders.js";
7
+ import { stripUndefined } from "./util.js";
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
+ }