@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
package/src/schema.ts ADDED
@@ -0,0 +1,260 @@
1
+ import { boolean, integer, jsonb, pgTable, text, timestamp, uuid, index, uniqueIndex } from "drizzle-orm/pg-core";
2
+
3
+ // ─── Vendors ─────────────────────────────────────────────────────────────────
4
+
5
+ export const vendors = pgTable("marketplace_vendors", {
6
+ id: uuid("id").defaultRandom().primaryKey(),
7
+ organizationId: text("organization_id").notNull(),
8
+ name: text("name").notNull(),
9
+ slug: text("slug"),
10
+ email: text("email"),
11
+ description: text("description"),
12
+ logoUrl: text("logo_url"),
13
+ bannerUrl: text("banner_url"),
14
+ contactPhone: text("contact_phone"),
15
+ businessAddress: jsonb("business_address").$type<{
16
+ line1?: string; line2?: string; city?: string; state?: string;
17
+ postalCode?: string; country?: string;
18
+ }>(),
19
+ bankAccount: jsonb("bank_account").$type<{
20
+ accountHolder?: string; bankName?: string; routingNumber?: string;
21
+ accountNumber?: string; iban?: string; swift?: string;
22
+ }>(),
23
+ taxId: text("tax_id"),
24
+ status: text("status").notNull().default("pending"),
25
+ verificationStatus: text("verification_status").notNull().default("unverified"),
26
+ rejectionReason: text("rejection_reason"),
27
+ approvedCategories: jsonb("approved_categories").$type<string[] | null>(),
28
+ tier: text("tier").notNull().default("standard"),
29
+ performanceScore: integer("performance_score").notNull().default(100),
30
+ suspensionReason: text("suspension_reason"),
31
+ suspendedAt: timestamp("suspended_at", { withTimezone: true }),
32
+ commissionRateBps: integer("commission_rate_bps").notNull().default(1000),
33
+ payoutSchedule: text("payout_schedule").notNull().default("weekly"),
34
+ payoutMinimumCents: integer("payout_minimum_cents").notNull().default(5000),
35
+ holdbackDays: integer("holdback_days").notNull().default(7),
36
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
37
+
38
+ // ─── Store Connection (links vendor to their Shopify/WooCommerce store) ─────
39
+ storeConnectionProvider: text("store_connection_provider"), // "shopify" | "woocommerce" | null
40
+ storeConnectionUrl: text("store_connection_url"),
41
+ storeAccessToken: text("store_access_token"), // Shopify token or WC consumer key
42
+ storeConsumerSecret: text("store_consumer_secret"), // WooCommerce consumer secret (null for Shopify)
43
+ storeWebhookSecret: text("store_webhook_secret"),
44
+ lastSyncAt: timestamp("last_sync_at", { withTimezone: true }),
45
+ syncStatus: text("sync_status").default("disconnected"), // "healthy" | "stale" | "error" | "disconnected"
46
+
47
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
48
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
49
+ }, (table) => ({
50
+ orgSlugUnique: uniqueIndex("marketplace_vendors_org_slug_unique").on(table.organizationId, table.slug),
51
+ orgIdx: index("idx_marketplace_vendors_org").on(table.organizationId),
52
+ }));
53
+
54
+ // ─── Vendor–Entity Links ─────────────────────────────────────────────────────
55
+
56
+ export const vendorEntities = pgTable("marketplace_vendor_entities", {
57
+ id: uuid("id").defaultRandom().primaryKey(),
58
+ vendorId: uuid("vendor_id").notNull().references(() => vendors.id, { onDelete: "cascade" }),
59
+ entityId: uuid("entity_id").notNull(), // FK: → sellable_entities.id (cross-package; skipped due to drizzle-orm version mismatch between core and plugin)
60
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
61
+ });
62
+
63
+ // ─── Vendor Documents ────────────────────────────────────────────────────────
64
+
65
+ export const vendorDocuments = pgTable("marketplace_vendor_documents", {
66
+ id: uuid("id").defaultRandom().primaryKey(),
67
+ vendorId: uuid("vendor_id").notNull().references(() => vendors.id, { onDelete: "cascade" }),
68
+ type: text("type").notNull(),
69
+ fileUrl: text("file_url").notNull(),
70
+ status: text("status").notNull().default("pending"),
71
+ reviewerNotes: text("reviewer_notes"),
72
+ uploadedAt: timestamp("uploaded_at", { withTimezone: true }).defaultNow().notNull(),
73
+ reviewedAt: timestamp("reviewed_at", { withTimezone: true }),
74
+ });
75
+
76
+ // ─── Commission Rules ────────────────────────────────────────────────────────
77
+
78
+ export const commissionRules = pgTable("marketplace_commission_rules", {
79
+ id: uuid("id").defaultRandom().primaryKey(),
80
+ name: text("name").notNull(),
81
+ type: text("type").notNull(),
82
+ categorySlug: text("category_slug"),
83
+ vendorId: uuid("vendor_id").references(() => vendors.id, { onDelete: "cascade" }),
84
+ vendorTier: text("vendor_tier"),
85
+ minVolumeCents: integer("min_volume_cents"),
86
+ maxVolumeCents: integer("max_volume_cents"),
87
+ rateBps: integer("rate_bps").notNull(),
88
+ validFrom: timestamp("valid_from", { withTimezone: true }),
89
+ validUntil: timestamp("valid_until", { withTimezone: true }),
90
+ priority: integer("priority").notNull().default(0),
91
+ isActive: boolean("is_active").notNull().default(true),
92
+ });
93
+
94
+ // ─── Sub-Orders ──────────────────────────────────────────────────────────────
95
+
96
+ export const vendorSubOrders = pgTable("marketplace_vendor_sub_orders", {
97
+ id: uuid("id").defaultRandom().primaryKey(),
98
+ orderId: uuid("order_id").notNull(), // FK: → orders.id (cross-package; skipped due to drizzle-orm version mismatch between core and plugin)
99
+ vendorId: uuid("vendor_id").notNull().references(() => vendors.id, { onDelete: "cascade" }),
100
+ status: text("status").notNull().default("pending"),
101
+ subtotal: integer("subtotal").notNull().default(0),
102
+ commissionAmount: integer("commission_amount").notNull().default(0),
103
+ payoutAmount: integer("payout_amount").notNull().default(0),
104
+ notified: boolean("notified").notNull().default(false),
105
+ lineItems: jsonb("line_items").$type<Array<{ entityId: string; quantity: number; totalPrice: number }>>().default([]),
106
+ trackingNumber: text("tracking_number"),
107
+ trackingUrl: text("tracking_url"),
108
+ carrier: text("carrier"),
109
+ fulfillmentStatus: text("fulfillment_status").notNull().default("unfulfilled"), // "unfulfilled" | "partially_fulfilled" | "fulfilled"
110
+ confirmedAt: timestamp("confirmed_at", { withTimezone: true }),
111
+ shippedAt: timestamp("shipped_at", { withTimezone: true }),
112
+ deliveredAt: timestamp("delivered_at", { withTimezone: true }),
113
+ cancelledAt: timestamp("cancelled_at", { withTimezone: true }),
114
+ cancellationReason: text("cancellation_reason"),
115
+ vendorNotes: text("vendor_notes"),
116
+
117
+ // ─── External Store Sync ────────────────────────────────────────────────────
118
+ externalOrderId: text("external_order_id"), // Order ID in vendor's external store
119
+ externalOrderUrl: text("external_order_url"), // Admin URL for vendor to view the order
120
+ externalSyncStatus: text("external_sync_status").default("pending"), // "pending" | "synced" | "failed"
121
+
122
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
123
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
124
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
125
+ });
126
+
127
+ // ─── Payouts ─────────────────────────────────────────────────────────────────
128
+
129
+ export const vendorPayouts = pgTable("marketplace_vendor_payouts", {
130
+ id: uuid("id").defaultRandom().primaryKey(),
131
+ vendorId: uuid("vendor_id").notNull().references(() => vendors.id, { onDelete: "cascade" }),
132
+ subOrderId: uuid("sub_order_id"),
133
+ amount: integer("amount").notNull().default(0),
134
+ status: text("status").notNull().default("pending"),
135
+ payoutMethod: text("payout_method"),
136
+ externalReference: text("external_reference"),
137
+ periodStart: timestamp("period_start", { withTimezone: true }),
138
+ periodEnd: timestamp("period_end", { withTimezone: true }),
139
+ grossAmount: integer("gross_amount"),
140
+ deductions: jsonb("deductions").$type<Array<{ type: string; amount: number; reference?: string }>>(),
141
+ netAmount: integer("net_amount"),
142
+ failedAt: timestamp("failed_at", { withTimezone: true }),
143
+ failureReason: text("failure_reason"),
144
+ retryCount: integer("retry_count").notNull().default(0),
145
+ processedAt: timestamp("processed_at", { withTimezone: true }),
146
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
147
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
148
+ });
149
+
150
+ // ─── Vendor Balance Ledger ───────────────────────────────────────────────────
151
+
152
+ export const vendorBalances = pgTable("marketplace_vendor_balances", {
153
+ id: uuid("id").defaultRandom().primaryKey(),
154
+ vendorId: uuid("vendor_id").notNull().references(() => vendors.id, { onDelete: "cascade" }),
155
+ type: text("type").notNull(),
156
+ amountCents: integer("amount_cents").notNull(),
157
+ runningBalanceCents: integer("running_balance_cents").notNull(),
158
+ referenceType: text("reference_type"),
159
+ referenceId: uuid("reference_id"),
160
+ description: text("description"),
161
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
162
+ });
163
+
164
+ // ─── Disputes ────────────────────────────────────────────────────────────────
165
+
166
+ export const disputes = pgTable("marketplace_disputes", {
167
+ id: uuid("id").defaultRandom().primaryKey(),
168
+ subOrderId: uuid("sub_order_id").notNull().references(() => vendorSubOrders.id, { onDelete: "cascade" }),
169
+ openedBy: text("opened_by").notNull(),
170
+ reason: text("reason").notNull(),
171
+ description: text("description"),
172
+ status: text("status").notNull().default("open"),
173
+ resolution: text("resolution"),
174
+ resolutionNotes: text("resolution_notes"),
175
+ refundAmountCents: integer("refund_amount_cents"),
176
+ evidence: jsonb("evidence").$type<Array<{ party: string; type: string; url?: string; note?: string; at: string }>>().default([]),
177
+ resolvedBy: text("resolved_by"),
178
+ deadlineAt: timestamp("deadline_at", { withTimezone: true }),
179
+ openedAt: timestamp("opened_at", { withTimezone: true }).defaultNow().notNull(),
180
+ resolvedAt: timestamp("resolved_at", { withTimezone: true }),
181
+ });
182
+
183
+ // ─── Vendor Reviews ──────────────────────────────────────────────────────────
184
+
185
+ export const vendorReviews = pgTable("marketplace_vendor_reviews", {
186
+ id: uuid("id").defaultRandom().primaryKey(),
187
+ vendorId: uuid("vendor_id").notNull().references(() => vendors.id, { onDelete: "cascade" }),
188
+ customerId: uuid("customer_id"),
189
+ orderId: uuid("order_id"),
190
+ rating: integer("rating").notNull(),
191
+ title: text("title"),
192
+ body: text("body"),
193
+ status: text("status").notNull().default("published"),
194
+ vendorResponse: text("vendor_response"),
195
+ vendorRespondedAt: timestamp("vendor_responded_at", { withTimezone: true }),
196
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
197
+ });
198
+
199
+ // ─── Return Requests ─────────────────────────────────────────────────────────
200
+
201
+ export const returnRequests = pgTable("marketplace_return_requests", {
202
+ id: uuid("id").defaultRandom().primaryKey(),
203
+ subOrderId: uuid("sub_order_id").notNull().references(() => vendorSubOrders.id, { onDelete: "cascade" }),
204
+ customerId: uuid("customer_id"),
205
+ reason: text("reason").notNull(),
206
+ description: text("description"),
207
+ status: text("status").notNull().default("requested"),
208
+ lineItems: jsonb("line_items").$type<Array<{ entityId: string; quantity: number; reason?: string }>>(),
209
+ refundAmountCents: integer("refund_amount_cents"),
210
+ vendorNotes: text("vendor_notes"),
211
+ trackingNumber: text("tracking_number"),
212
+ requestedAt: timestamp("requested_at", { withTimezone: true }).defaultNow().notNull(),
213
+ resolvedAt: timestamp("resolved_at", { withTimezone: true }),
214
+ });
215
+
216
+ // ─── RFQ (B2B) ──────────────────────────────────────────────────────────────
217
+
218
+ export const rfqs = pgTable("marketplace_rfq", {
219
+ id: uuid("id").defaultRandom().primaryKey(),
220
+ buyerId: uuid("buyer_id"),
221
+ title: text("title").notNull(),
222
+ description: text("description"),
223
+ categorySlug: text("category_slug"),
224
+ quantity: integer("quantity"),
225
+ budgetCents: integer("budget_cents"),
226
+ currency: text("currency").notNull().default("USD"),
227
+ deadlineAt: timestamp("deadline_at", { withTimezone: true }),
228
+ status: text("status").notNull().default("open"),
229
+ awardedVendorId: uuid("awarded_vendor_id"),
230
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
231
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
232
+ });
233
+
234
+ export const rfqResponses = pgTable("marketplace_rfq_responses", {
235
+ id: uuid("id").defaultRandom().primaryKey(),
236
+ rfqId: uuid("rfq_id").notNull().references(() => rfqs.id, { onDelete: "cascade" }),
237
+ vendorId: uuid("vendor_id").notNull().references(() => vendors.id, { onDelete: "cascade" }),
238
+ unitPriceCents: integer("unit_price_cents").notNull(),
239
+ totalPriceCents: integer("total_price_cents").notNull(),
240
+ leadTimeDays: integer("lead_time_days"),
241
+ notes: text("notes"),
242
+ status: text("status").notNull().default("submitted"),
243
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
244
+ });
245
+
246
+ // ─── Contract Prices (B2B) ───────────────────────────────────────────────────
247
+
248
+ export const contractPrices = pgTable("marketplace_contract_prices", {
249
+ id: uuid("id").defaultRandom().primaryKey(),
250
+ vendorId: uuid("vendor_id").notNull().references(() => vendors.id, { onDelete: "cascade" }),
251
+ buyerId: uuid("buyer_id").notNull(),
252
+ entityId: uuid("entity_id").notNull(),
253
+ variantId: uuid("variant_id"),
254
+ priceCents: integer("price_cents").notNull(),
255
+ minQuantity: integer("min_quantity").notNull().default(1),
256
+ currency: text("currency").notNull().default("USD"),
257
+ validFrom: timestamp("valid_from", { withTimezone: true }),
258
+ validUntil: timestamp("valid_until", { withTimezone: true }),
259
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
260
+ });
@@ -0,0 +1,238 @@
1
+ import { z, createRoute } from "@hono/zod-openapi";
2
+
3
+ const ErrorSchema = z.object({
4
+ error: z.object({
5
+ code: z.string(),
6
+ message: z.string(),
7
+ }),
8
+ });
9
+
10
+ const errorResponses = {
11
+ 401: { content: { "application/json": { schema: ErrorSchema } }, description: "Authentication required." },
12
+ 403: { content: { "application/json": { schema: ErrorSchema } }, description: "Insufficient permissions." },
13
+ 404: { content: { "application/json": { schema: ErrorSchema } }, description: "Not found." },
14
+ 422: { content: { "application/json": { schema: ErrorSchema } }, description: "Validation error." },
15
+ 500: { content: { "application/json": { schema: ErrorSchema } }, description: "Server error." },
16
+ } as const;
17
+
18
+ const DataResponseSchema = z.object({ data: z.any() });
19
+
20
+ // ═══════════════════════════════════════════════════════════════════════════════
21
+ // RFQ (Request for Quote)
22
+ // ═══════════════════════════════════════════════════════════════════════════════
23
+
24
+ // ─── Create RFQ ─────────────────────────────────────────────────────────────
25
+
26
+ export const CreateRFQBodySchema = z.object({
27
+ title: z.string().min(1).openapi({ example: "Bulk order: 500 widgets" }),
28
+ buyerId: z.string().optional(),
29
+ description: z.string().optional(),
30
+ categorySlug: z.string().optional(),
31
+ quantity: z.number().int().optional().openapi({ example: 500 }),
32
+ budgetCents: z.number().int().optional().openapi({ example: 500000 }),
33
+ currency: z.string().optional().openapi({ example: "USD" }),
34
+ deadlineAt: z.string().optional().openapi({ example: "2026-04-01T00:00:00Z", description: "ISO 8601 date string" }),
35
+ metadata: z.record(z.string(), z.unknown()).optional(),
36
+ }).openapi("CreateRFQRequest");
37
+
38
+ export const createRFQRoute = createRoute({
39
+ method: "post",
40
+ path: "/api/marketplace/rfq",
41
+ tags: ["Marketplace - B2B"],
42
+ summary: "Create a Request for Quote",
43
+ request: {
44
+ body: { content: { "application/json": { schema: CreateRFQBodySchema } }, required: true },
45
+ },
46
+ responses: {
47
+ 201: { content: { "application/json": { schema: DataResponseSchema } }, description: "RFQ created." },
48
+ ...errorResponses,
49
+ },
50
+ });
51
+
52
+ // ─── Respond to RFQ ─────────────────────────────────────────────────────────
53
+
54
+ export const RespondRFQBodySchema = z.object({
55
+ vendorId: z.string().min(1).openapi({ example: "vendor_abc" }),
56
+ unitPriceCents: z.number().int().openapi({ example: 800 }),
57
+ totalPriceCents: z.number().int().openapi({ example: 400000 }),
58
+ leadTimeDays: z.number().int().optional().openapi({ example: 14 }),
59
+ notes: z.string().optional(),
60
+ }).openapi("RespondRFQRequest");
61
+
62
+ export const respondRFQRoute = createRoute({
63
+ method: "post",
64
+ path: "/api/marketplace/rfq/{id}/respond",
65
+ tags: ["Marketplace - B2B"],
66
+ summary: "Submit a vendor response to an RFQ",
67
+ request: {
68
+ params: z.object({ id: z.string() }),
69
+ body: { content: { "application/json": { schema: RespondRFQBodySchema } }, required: true },
70
+ },
71
+ responses: {
72
+ 201: { content: { "application/json": { schema: DataResponseSchema } }, description: "RFQ response submitted." },
73
+ ...errorResponses,
74
+ },
75
+ });
76
+
77
+ // ─── Award RFQ ──────────────────────────────────────────────────────────────
78
+
79
+ export const AwardRFQBodySchema = z.object({
80
+ vendorId: z.string().min(1).openapi({ example: "vendor_abc" }),
81
+ }).openapi("AwardRFQRequest");
82
+
83
+ export const awardRFQRoute = createRoute({
84
+ method: "post",
85
+ path: "/api/marketplace/rfq/{id}/award",
86
+ tags: ["Marketplace - B2B"],
87
+ summary: "Award an RFQ to a vendor",
88
+ request: {
89
+ params: z.object({ id: z.string() }),
90
+ body: { content: { "application/json": { schema: AwardRFQBodySchema } }, required: true },
91
+ },
92
+ responses: {
93
+ 200: { content: { "application/json": { schema: DataResponseSchema } }, description: "RFQ awarded." },
94
+ ...errorResponses,
95
+ },
96
+ });
97
+
98
+ // ═══════════════════════════════════════════════════════════════════════════════
99
+ // CONTRACT PRICES
100
+ // ═══════════════════════════════════════════════════════════════════════════════
101
+
102
+ // ─── Create Contract Price ──────────────────────────────────────────────────
103
+
104
+ export const CreateContractPriceBodySchema = z.object({
105
+ vendorId: z.string().min(1).openapi({ example: "vendor_abc" }),
106
+ buyerId: z.string().min(1).openapi({ example: "buyer_xyz" }),
107
+ entityId: z.string().min(1).openapi({ example: "product_001" }),
108
+ variantId: z.string().optional(),
109
+ priceCents: z.number().int().openapi({ example: 7500 }),
110
+ minQuantity: z.number().int().optional().openapi({ example: 100 }),
111
+ currency: z.string().optional().openapi({ example: "USD" }),
112
+ validFrom: z.string().optional().openapi({ example: "2026-01-01T00:00:00Z", description: "ISO 8601 date string" }),
113
+ validUntil: z.string().optional().openapi({ example: "2026-12-31T23:59:59Z", description: "ISO 8601 date string" }),
114
+ }).openapi("CreateContractPriceRequest");
115
+
116
+ export const createContractPriceRoute = createRoute({
117
+ method: "post",
118
+ path: "/api/marketplace/contract-prices",
119
+ tags: ["Marketplace - B2B"],
120
+ summary: "Create a contract price",
121
+ request: {
122
+ body: { content: { "application/json": { schema: CreateContractPriceBodySchema } }, required: true },
123
+ },
124
+ responses: {
125
+ 201: { content: { "application/json": { schema: DataResponseSchema } }, description: "Contract price created." },
126
+ ...errorResponses,
127
+ },
128
+ });
129
+
130
+ // ─── Update Contract Price ──────────────────────────────────────────────────
131
+
132
+ export const UpdateContractPriceBodySchema = z.object({
133
+ priceCents: z.number().int().optional().openapi({ example: 7000 }),
134
+ minQuantity: z.number().int().optional(),
135
+ validFrom: z.string().nullable().optional().openapi({ description: "ISO 8601 date string or null to clear" }),
136
+ validUntil: z.string().nullable().optional().openapi({ description: "ISO 8601 date string or null to clear" }),
137
+ }).openapi("UpdateContractPriceRequest");
138
+
139
+ export const updateContractPriceRoute = createRoute({
140
+ method: "patch",
141
+ path: "/api/marketplace/contract-prices/{id}",
142
+ tags: ["Marketplace - B2B"],
143
+ summary: "Update a contract price",
144
+ request: {
145
+ params: z.object({ id: z.string() }),
146
+ body: { content: { "application/json": { schema: UpdateContractPriceBodySchema } }, required: true },
147
+ },
148
+ responses: {
149
+ 200: { content: { "application/json": { schema: DataResponseSchema } }, description: "Contract price updated." },
150
+ ...errorResponses,
151
+ },
152
+ });
153
+
154
+ // ─── List RFQs ──────────────────────────────────────────────────────────────
155
+
156
+ export const listRFQsRoute = createRoute({
157
+ method: "get",
158
+ path: "/api/marketplace/rfq",
159
+ tags: ["Marketplace - B2B"],
160
+ summary: "List Requests for Quote",
161
+ request: {
162
+ query: z.object({
163
+ status: z.string().optional(),
164
+ categorySlug: z.string().optional(),
165
+ }),
166
+ },
167
+ responses: {
168
+ 200: { content: { "application/json": { schema: DataResponseSchema } }, description: "Success" },
169
+ ...errorResponses,
170
+ },
171
+ });
172
+
173
+ // ─── Get RFQ ────────────────────────────────────────────────────────────────
174
+
175
+ export const getRFQRoute = createRoute({
176
+ method: "get",
177
+ path: "/api/marketplace/rfq/{id}",
178
+ tags: ["Marketplace - B2B"],
179
+ summary: "Get RFQ detail",
180
+ request: {
181
+ params: z.object({ id: z.string() }),
182
+ },
183
+ responses: {
184
+ 200: { content: { "application/json": { schema: DataResponseSchema } }, description: "Success" },
185
+ ...errorResponses,
186
+ },
187
+ });
188
+
189
+ // ─── Close RFQ ──────────────────────────────────────────────────────────────
190
+
191
+ export const closeRFQRoute = createRoute({
192
+ method: "post",
193
+ path: "/api/marketplace/rfq/{id}/close",
194
+ tags: ["Marketplace - B2B"],
195
+ summary: "Close an RFQ",
196
+ request: {
197
+ params: z.object({ id: z.string() }),
198
+ },
199
+ responses: {
200
+ 200: { content: { "application/json": { schema: DataResponseSchema } }, description: "RFQ closed." },
201
+ ...errorResponses,
202
+ },
203
+ });
204
+
205
+ // ─── List Contract Prices ───────────────────────────────────────────────────
206
+
207
+ export const listContractPricesRoute = createRoute({
208
+ method: "get",
209
+ path: "/api/marketplace/contract-prices",
210
+ tags: ["Marketplace - B2B"],
211
+ summary: "List contract prices",
212
+ request: {
213
+ query: z.object({
214
+ vendorId: z.string().optional(),
215
+ buyerId: z.string().optional(),
216
+ }),
217
+ },
218
+ responses: {
219
+ 200: { content: { "application/json": { schema: DataResponseSchema } }, description: "Success" },
220
+ ...errorResponses,
221
+ },
222
+ });
223
+
224
+ // ─── Delete Contract Price ──────────────────────────────────────────────────
225
+
226
+ export const deleteContractPriceRoute = createRoute({
227
+ method: "delete",
228
+ path: "/api/marketplace/contract-prices/{id}",
229
+ tags: ["Marketplace - B2B"],
230
+ summary: "Delete a contract price",
231
+ request: {
232
+ params: z.object({ id: z.string() }),
233
+ },
234
+ responses: {
235
+ 200: { content: { "application/json": { schema: DataResponseSchema } }, description: "Contract price deleted." },
236
+ ...errorResponses,
237
+ },
238
+ });
@@ -0,0 +1,129 @@
1
+ import { z, createRoute } from "@hono/zod-openapi";
2
+
3
+ const ErrorSchema = z.object({
4
+ error: z.object({
5
+ code: z.string(),
6
+ message: z.string(),
7
+ }),
8
+ });
9
+
10
+ const errorResponses = {
11
+ 401: { content: { "application/json": { schema: ErrorSchema } }, description: "Authentication required." },
12
+ 403: { content: { "application/json": { schema: ErrorSchema } }, description: "Insufficient permissions." },
13
+ 404: { content: { "application/json": { schema: ErrorSchema } }, description: "Not found." },
14
+ 422: { content: { "application/json": { schema: ErrorSchema } }, description: "Validation error." },
15
+ 500: { content: { "application/json": { schema: ErrorSchema } }, description: "Server error." },
16
+ } as const;
17
+
18
+ const CommissionResponseSchema = z.object({ data: z.any() });
19
+
20
+ // ─── Create Commission Rule ──────────────────────────────────────────────────
21
+
22
+ export const CreateCommissionRuleBodySchema = z.object({
23
+ name: z.string().min(1).openapi({ example: "Electronics Category Rate" }),
24
+ type: z.enum(["category", "volume_tier", "vendor_tier", "promotional"]).openapi({ example: "category" }),
25
+ rateBps: z.number().int().min(0).max(10000).openapi({ example: 1500, description: "Basis points (100 = 1%)" }),
26
+ categorySlug: z.string().optional(),
27
+ vendorId: z.uuid().optional(),
28
+ vendorTier: z.string().optional(),
29
+ minVolumeCents: z.number().int().optional(),
30
+ maxVolumeCents: z.number().int().optional(),
31
+ validFrom: z.string().optional().openapi({ example: "2026-01-01T00:00:00Z", description: "ISO 8601 date string" }),
32
+ validUntil: z.string().optional().openapi({ example: "2026-12-31T23:59:59Z", description: "ISO 8601 date string" }),
33
+ priority: z.number().int().optional(),
34
+ }).openapi("CreateCommissionRuleRequest");
35
+
36
+ export const createCommissionRuleRoute = createRoute({
37
+ method: "post",
38
+ path: "/api/marketplace/commission-rules",
39
+ tags: ["Marketplace - Commission"],
40
+ summary: "Create a commission rule",
41
+ request: {
42
+ body: { content: { "application/json": { schema: CreateCommissionRuleBodySchema } }, required: true },
43
+ },
44
+ responses: {
45
+ 201: { content: { "application/json": { schema: CommissionResponseSchema } }, description: "Commission rule created." },
46
+ ...errorResponses,
47
+ },
48
+ });
49
+
50
+ // ─── Update Commission Rule ──────────────────────────────────────────────────
51
+
52
+ export const UpdateCommissionRuleBodySchema = z.object({
53
+ name: z.string().min(1).optional(),
54
+ rateBps: z.number().int().min(0).max(10000).optional(),
55
+ categorySlug: z.string().nullable().optional(),
56
+ vendorTier: z.string().nullable().optional(),
57
+ minVolumeCents: z.number().int().nullable().optional(),
58
+ maxVolumeCents: z.number().int().nullable().optional(),
59
+ validFrom: z.string().nullable().optional().openapi({ description: "ISO 8601 date string or null to clear" }),
60
+ validUntil: z.string().nullable().optional().openapi({ description: "ISO 8601 date string or null to clear" }),
61
+ priority: z.number().int().optional(),
62
+ isActive: z.boolean().optional(),
63
+ }).openapi("UpdateCommissionRuleRequest");
64
+
65
+ export const updateCommissionRuleRoute = createRoute({
66
+ method: "patch",
67
+ path: "/api/marketplace/commission-rules/{id}",
68
+ tags: ["Marketplace - Commission"],
69
+ summary: "Update a commission rule",
70
+ request: {
71
+ params: z.object({ id: z.uuid() }),
72
+ body: { content: { "application/json": { schema: UpdateCommissionRuleBodySchema } }, required: true },
73
+ },
74
+ responses: {
75
+ 200: { content: { "application/json": { schema: CommissionResponseSchema } }, description: "Commission rule updated." },
76
+ ...errorResponses,
77
+ },
78
+ });
79
+
80
+ // ─── Preview Commission Rate ─────────────────────────────────────────────────
81
+
82
+ export const PreviewCommissionBodySchema = z.object({
83
+ vendorId: z.uuid().openapi({ example: "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a" }),
84
+ categorySlug: z.string().optional(),
85
+ volumeCents: z.number().int().optional().openapi({ example: 100000, description: "Order volume in cents" }),
86
+ }).openapi("PreviewCommissionRequest");
87
+
88
+ export const previewCommissionRoute = createRoute({
89
+ method: "post",
90
+ path: "/api/marketplace/commission-rules/preview",
91
+ tags: ["Marketplace - Commission"],
92
+ summary: "Preview the commission rate for a vendor",
93
+ request: {
94
+ body: { content: { "application/json": { schema: PreviewCommissionBodySchema } }, required: true },
95
+ },
96
+ responses: {
97
+ 200: { content: { "application/json": { schema: CommissionResponseSchema } }, description: "Commission rate preview." },
98
+ ...errorResponses,
99
+ },
100
+ });
101
+
102
+ // ─── List Commission Rules ──────────────────────────────────────────────────
103
+
104
+ export const listCommissionRulesRoute = createRoute({
105
+ method: "get",
106
+ path: "/api/marketplace/commission-rules",
107
+ tags: ["Marketplace - Commission"],
108
+ summary: "List all commission rules",
109
+ responses: {
110
+ 200: { content: { "application/json": { schema: CommissionResponseSchema } }, description: "Success" },
111
+ ...errorResponses,
112
+ },
113
+ });
114
+
115
+ // ─── Delete Commission Rule ─────────────────────────────────────────────────
116
+
117
+ export const deleteCommissionRuleRoute = createRoute({
118
+ method: "delete",
119
+ path: "/api/marketplace/commission-rules/{id}",
120
+ tags: ["Marketplace - Commission"],
121
+ summary: "Delete a commission rule",
122
+ request: {
123
+ params: z.object({ id: z.uuid() }),
124
+ },
125
+ responses: {
126
+ 200: { content: { "application/json": { schema: CommissionResponseSchema } }, description: "Commission rule deleted." },
127
+ ...errorResponses,
128
+ },
129
+ });