@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,67 @@
1
+ export type SubOrderStatus = "pending" | "confirmed" | "processing" | "shipped" | "delivered" | "cancelled";
2
+ export declare const SUB_ORDER_TRANSITIONS: Record<SubOrderStatus, SubOrderStatus[]>;
3
+ export type VendorStatus = "pending" | "approved" | "suspended";
4
+ export type VerificationStatus = "unverified" | "documents_submitted" | "verified" | "rejected";
5
+ export type VendorTier = "standard" | "silver" | "gold" | "platinum";
6
+ export type PayoutSchedule = "daily" | "weekly" | "biweekly" | "monthly" | "manual";
7
+ export type VendorApprovalMode = "manual" | "auto" | "invitation";
8
+ export type CommissionRuleType = "category" | "volume_tier" | "vendor_tier" | "promotional";
9
+ export type DisputeStatus = "open" | "vendor_response_pending" | "platform_review" | "resolved" | "escalated" | "closed";
10
+ export type DisputeReason = "item_not_received" | "item_not_as_described" | "defective" | "wrong_item" | "other";
11
+ export type DisputeResolution = "refund_full" | "refund_partial" | "replacement" | "rejected" | "vendor_favor" | "buyer_favor";
12
+ export type ReturnStatus = "requested" | "vendor_approved" | "vendor_rejected" | "shipped_back" | "received" | "refunded" | "closed";
13
+ export type ReturnReason = "defective" | "wrong_item" | "not_as_described" | "changed_mind" | "other";
14
+ export type BalanceEntryType = "sale" | "commission" | "refund_deduction" | "adjustment" | "payout";
15
+ export type DocumentType = "business_license" | "tax_form" | "bank_proof" | "identity" | "other";
16
+ export type DocumentStatus = "pending" | "approved" | "rejected";
17
+ export type ReviewStatus = "pending" | "published" | "hidden" | "flagged";
18
+ export type RFQStatus = "open" | "closed" | "awarded" | "cancelled";
19
+ export type RFQResponseStatus = "submitted" | "shortlisted" | "accepted" | "rejected" | "withdrawn";
20
+ export interface MarketplacePluginOptions {
21
+ defaultCommissionRateBps?: number;
22
+ vendorApprovalMode?: VendorApprovalMode;
23
+ requiredDocuments?: DocumentType[];
24
+ defaultPayoutSchedule?: PayoutSchedule;
25
+ defaultPayoutMinimumCents?: number;
26
+ defaultHoldbackDays?: number;
27
+ vendorResponseDeadlineDays?: number;
28
+ autoEscalateOnMissedDeadline?: boolean;
29
+ returnWindowDays?: number;
30
+ autoApproveReturnsOnVendorTimeout?: boolean;
31
+ vendorReturnResponseDays?: number;
32
+ requireVerifiedPurchase?: boolean;
33
+ reviewModerationEnabled?: boolean;
34
+ b2b?: {
35
+ rfq?: boolean;
36
+ contractPricing?: boolean;
37
+ };
38
+ performanceThresholds?: {
39
+ minRating?: number;
40
+ maxDefectRatePercent?: number;
41
+ maxLateShipmentRatePercent?: number;
42
+ maxCancellationRatePercent?: number;
43
+ };
44
+ }
45
+ /**
46
+ * Driver-agnostic Drizzle PostgreSQL database type.
47
+ *
48
+ * `PgDatabase` from `drizzle-orm/pg-core` is the base class that all PG drivers
49
+ * extend (postgres-js, pglite, node-postgres). Using it with
50
+ * `Record<string, unknown>` as the schema generic means:
51
+ * - Row types are fully inferred from `pgTable` schema objects (`.from(vendors)`)
52
+ * - No coupling to any specific driver package
53
+ * - Works identically with PGlite in tests and postgres-js in production
54
+ */
55
+ export type { PluginDb as Db } from "@unifiedcommerce/core";
56
+ /** Minimal Hono-compatible context for route handlers */
57
+ export interface RouteContext {
58
+ req: {
59
+ json(): Promise<Record<string, unknown>>;
60
+ param(name: string): string;
61
+ query(name: string): string | undefined;
62
+ };
63
+ json(data: unknown, status?: number): Response;
64
+ get(key: string): unknown;
65
+ }
66
+ export declare function errorMessage(err: unknown): string;
67
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,13 @@
1
+ // ─── Sub-Order Status ────────────────────────────────────────────────────────
2
+ export const SUB_ORDER_TRANSITIONS = {
3
+ pending: ["confirmed", "cancelled"],
4
+ confirmed: ["processing", "cancelled"],
5
+ processing: ["shipped", "cancelled"],
6
+ shipped: ["delivered"],
7
+ delivered: [],
8
+ cancelled: [],
9
+ };
10
+ // ─── Error helper ────────────────────────────────────────────────────────────
11
+ export function errorMessage(err) {
12
+ return err instanceof Error ? err.message : "Internal server error";
13
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@unifiedcommerce/plugin-marketplace",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "default": "./dist/index.js"
9
+ },
10
+ "./schema": {
11
+ "types": "./dist/schema.d.ts",
12
+ "default": "./dist/schema.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.build.json",
17
+ "check-types": "tsc --noEmit",
18
+ "lint": "eslint . --max-warnings 1000",
19
+ "test": "vitest run"
20
+ },
21
+ "dependencies": {
22
+ "@hono/zod-openapi": "^1.2.2",
23
+ "@unifiedcommerce/core": "*",
24
+ "drizzle-orm": "^0.45.1",
25
+ "hono": "^4.12.5"
26
+ },
27
+ "devDependencies": {
28
+ "@repo/eslint-config": "*",
29
+ "@repo/typescript-config": "*",
30
+ "@types/node": "^24.5.2",
31
+ "eslint": "^9.39.1",
32
+ "typescript": "5.9.2",
33
+ "vitest": "^3.2.4"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "src",
41
+ "README.md"
42
+ ]
43
+ }
@@ -0,0 +1,75 @@
1
+ import type { AnalyticsModel } from "@unifiedcommerce/core";
2
+
3
+ /**
4
+ * Marketplace analytics models — SQL-based definitions for vendor-scoped analytics.
5
+ *
6
+ * These models describe the marketplace tables (marketplace_vendor_sub_orders,
7
+ * marketplace_vendor_balances, marketplace_vendor_reviews) and are registered
8
+ * on the DrizzleAnalyticsAdapter via the plugin's analyticsModels manifest slot.
9
+ */
10
+
11
+ export const VENDOR_ORDERS_MODEL: AnalyticsModel = {
12
+ name: "VendorOrders",
13
+ table: "marketplace_vendor_sub_orders",
14
+ scopeRules: [
15
+ { role: "vendor", filter: "vendor_id = :vendorId" },
16
+ ],
17
+ measures: {
18
+ count: { type: "count" },
19
+ revenue: { sql: "subtotal", type: "sum" },
20
+ commissionPaid: { sql: "commission_amount", type: "sum" },
21
+ netPayout: { sql: "payout_amount", type: "sum" },
22
+ },
23
+ dimensions: {
24
+ id: { sql: "id", type: "string" },
25
+ vendorId: { sql: "vendor_id", type: "string" },
26
+ orderId: { sql: "order_id", type: "string" },
27
+ status: { sql: "status", type: "string" },
28
+ createdAt: { sql: "created_at", type: "time" },
29
+ },
30
+ };
31
+
32
+ export const VENDOR_BALANCE_MODEL: AnalyticsModel = {
33
+ name: "VendorBalance",
34
+ table: "marketplace_vendor_balances",
35
+ scopeRules: [
36
+ { role: "vendor", filter: "vendor_id = :vendorId" },
37
+ ],
38
+ measures: {
39
+ totalCredits: { sql: "CASE WHEN amount_cents > 0 THEN amount_cents ELSE 0 END", type: "sum" },
40
+ totalDebits: { sql: "CASE WHEN amount_cents < 0 THEN ABS(amount_cents) ELSE 0 END", type: "sum" },
41
+ netBalance: { sql: "amount_cents", type: "sum" },
42
+ entryCount: { type: "count" },
43
+ },
44
+ dimensions: {
45
+ id: { sql: "id", type: "string" },
46
+ vendorId: { sql: "vendor_id", type: "string" },
47
+ type: { sql: "type", type: "string" },
48
+ createdAt: { sql: "created_at", type: "time" },
49
+ },
50
+ };
51
+
52
+ export const VENDOR_REVIEWS_MODEL: AnalyticsModel = {
53
+ name: "VendorReviews",
54
+ table: "marketplace_vendor_reviews",
55
+ scopeRules: [
56
+ { role: "vendor", filter: "vendor_id = :vendorId" },
57
+ ],
58
+ measures: {
59
+ count: { type: "count" },
60
+ averageRating: { sql: "rating", type: "avg" },
61
+ },
62
+ dimensions: {
63
+ id: { sql: "id", type: "string" },
64
+ vendorId: { sql: "vendor_id", type: "string" },
65
+ rating: { sql: "rating", type: "number" },
66
+ status: { sql: "status", type: "string" },
67
+ createdAt: { sql: "created_at", type: "time" },
68
+ },
69
+ };
70
+
71
+ export const MARKETPLACE_ANALYTICS_MODELS: AnalyticsModel[] = [
72
+ VENDOR_ORDERS_MODEL,
73
+ VENDOR_BALANCE_MODEL,
74
+ VENDOR_REVIEWS_MODEL,
75
+ ];
package/src/hooks.ts ADDED
@@ -0,0 +1,215 @@
1
+ import { eq } from "drizzle-orm";
2
+ import type { PluginHookRegistration } from "@unifiedcommerce/core";
3
+ import { vendors, vendorEntities, vendorSubOrders, vendorPayouts } from "./schema";
4
+ import { CommissionService } from "./services/commission";
5
+ import { PayoutService } from "./services/payout";
6
+ import type { Db, MarketplacePluginOptions } from "./types";
7
+
8
+ function getDbFromHookArgs(args: unknown): Db {
9
+ const a = args as { context: { services: { database: { db: unknown } } } };
10
+ return a.context.services.database.db as Db;
11
+ }
12
+
13
+ export function buildHooks(options: MarketplacePluginOptions): PluginHookRegistration[] {
14
+ return [
15
+ // ─── catalog.beforeCreate ──────────────────────────────────────────────
16
+ {
17
+ key: "catalog.beforeCreate",
18
+ async handler(args: unknown) {
19
+ const { data } = args as { data: Record<string, unknown> };
20
+ const metadata = data?.metadata as Record<string, unknown> | undefined;
21
+ const vendorId = metadata?.vendorId;
22
+ if (!vendorId) return data;
23
+
24
+ const db = getDbFromHookArgs(args);
25
+ const [vendor] = await db.select().from(vendors).where(eq(vendors.id, String(vendorId)));
26
+
27
+ if (!vendor) throw new Error("Vendor not found.");
28
+ if (vendor.status === "suspended") throw new Error("Vendor is suspended and cannot create listings.");
29
+ if (vendor.status !== "approved") throw new Error("Vendor must be approved before creating marketplace listings.");
30
+
31
+ // Check approved categories if set
32
+ const approvedCats = vendor.approvedCategories as string[] | null;
33
+ if (approvedCats && Array.isArray(approvedCats)) {
34
+ const entityType = data.type as string | undefined;
35
+ // approvedCategories holds category slugs; entityType may match
36
+ // This is a basic check; real implementations would check category assignment
37
+ }
38
+
39
+ return data;
40
+ },
41
+ },
42
+
43
+ // ─── catalog.afterCreate ───────────────────────────────────────────────
44
+ {
45
+ key: "catalog.afterCreate",
46
+ async handler(args: unknown) {
47
+ const { result } = args as { result: { id: string; metadata?: Record<string, unknown> | null } };
48
+ const vendorId = result?.metadata?.vendorId;
49
+ if (!vendorId) return;
50
+
51
+ const db = getDbFromHookArgs(args);
52
+ await db.insert(vendorEntities).values({
53
+ vendorId: String(vendorId),
54
+ entityId: result.id,
55
+ });
56
+ },
57
+ },
58
+
59
+ // ─── catalog.beforeList ────────────────────────────────────────────────
60
+ {
61
+ key: "catalog.beforeList",
62
+ async handler(args: unknown) {
63
+ const { data, context: hookContext } = args as {
64
+ data: unknown;
65
+ context: { context: Record<string, unknown>; actor?: { vendorId?: string | null } };
66
+ };
67
+ hookContext.context.marketplaceVendorScope = hookContext.actor?.vendorId ?? null;
68
+ return data;
69
+ },
70
+ },
71
+
72
+ // ─── catalog.afterList ─────────────────────────────────────────────────
73
+ {
74
+ key: "catalog.afterList",
75
+ async handler(args: unknown) {
76
+ const { result, context: hookContext } = args as {
77
+ result: { items?: Array<{ id: string }> };
78
+ context: { context: Record<string, unknown> };
79
+ };
80
+ const vendorScope = hookContext.context.marketplaceVendorScope as string | null;
81
+ if (!vendorScope || !Array.isArray(result?.items)) return;
82
+
83
+ const db = getDbFromHookArgs(args);
84
+ const links = await db.select().from(vendorEntities).where(eq(vendorEntities.vendorId, vendorScope));
85
+ const linkedEntityIds = new Set(links.map((l: { entityId: string }) => l.entityId));
86
+ result.items = result.items.filter((item) => linkedEntityIds.has(item.id));
87
+ },
88
+ },
89
+
90
+ // ─── catalog.afterRead ─────────────────────────────────────────────────
91
+ {
92
+ key: "catalog.afterRead",
93
+ async handler(args: unknown) {
94
+ const { result } = args as { result: Record<string, unknown> & { id: string } };
95
+ const db = getDbFromHookArgs(args);
96
+ const [link] = await db.select().from(vendorEntities).where(eq(vendorEntities.entityId, result.id));
97
+ if (!link) return;
98
+
99
+ const [vendor] = await db.select().from(vendors).where(eq(vendors.id, link.vendorId));
100
+ if (!vendor) return;
101
+
102
+ result.marketplace = {
103
+ vendor: {
104
+ id: vendor.id,
105
+ name: vendor.name,
106
+ slug: vendor.slug,
107
+ status: vendor.status,
108
+ tier: vendor.tier,
109
+ logoUrl: vendor.logoUrl,
110
+ },
111
+ };
112
+ },
113
+ },
114
+
115
+ // ─── orders.afterCreate ────────────────────────────────────────────────
116
+ {
117
+ key: "orders.afterCreate",
118
+ async handler(args: unknown) {
119
+ const { result, context: hookContext } = args as {
120
+ result: { id: string; lineItems?: Array<{ entityId: string; quantity: number; totalPrice: number }> };
121
+ context: { logger: { info(msg: string, data?: unknown): void } };
122
+ };
123
+ const db = getDbFromHookArgs(args);
124
+ const commissionService = new CommissionService(db, options);
125
+ const payoutService = new PayoutService(db, options);
126
+
127
+ const grouped = new Map<string, Array<{ entityId: string; quantity: number; totalPrice: number }>>();
128
+
129
+ for (const lineItem of result.lineItems ?? []) {
130
+ const [link] = await db.select().from(vendorEntities).where(eq(vendorEntities.entityId, lineItem.entityId));
131
+ if (!link) continue;
132
+ const [vendor] = await db.select().from(vendors).where(eq(vendors.id, link.vendorId));
133
+ if (!vendor) continue;
134
+
135
+ const existing = grouped.get(vendor.id) ?? [];
136
+ existing.push({
137
+ entityId: lineItem.entityId,
138
+ quantity: lineItem.quantity,
139
+ totalPrice: lineItem.totalPrice,
140
+ });
141
+ grouped.set(vendor.id, existing);
142
+ }
143
+
144
+ for (const [vendorId, lineItems] of grouped.entries()) {
145
+ const subtotal = lineItems.reduce((sum, item) => sum + item.totalPrice, 0);
146
+
147
+ // Resolve commission via rules engine
148
+ const commissionRateBps = await commissionService.resolveRate(vendorId);
149
+ const commissionAmount = Math.round((subtotal * commissionRateBps) / 10000);
150
+ const payoutAmount = subtotal - commissionAmount;
151
+
152
+ const [subOrder] = await db.insert(vendorSubOrders).values({
153
+ orderId: result.id,
154
+ vendorId,
155
+ status: "pending",
156
+ subtotal,
157
+ commissionAmount,
158
+ payoutAmount,
159
+ notified: true,
160
+ lineItems,
161
+ metadata: {},
162
+ }).returning();
163
+
164
+ if (!subOrder) continue;
165
+
166
+ // Credit vendor balance with full subtotal
167
+ await payoutService.addLedgerEntry({
168
+ vendorId,
169
+ type: "sale",
170
+ amountCents: subtotal,
171
+ referenceType: "sub_order",
172
+ referenceId: subOrder.id,
173
+ description: `Sale from order ${result.id.slice(0, 8)}`,
174
+ });
175
+
176
+ // Record commission as separate entry
177
+ await payoutService.addLedgerEntry({
178
+ vendorId,
179
+ type: "commission",
180
+ amountCents: -commissionAmount,
181
+ referenceType: "sub_order",
182
+ referenceId: subOrder.id,
183
+ description: `Commission (${commissionRateBps}bps) on order ${result.id.slice(0, 8)}`,
184
+ });
185
+
186
+ hookContext.logger.info("marketplace_sub_order_created", {
187
+ orderId: result.id,
188
+ subOrderId: subOrder.id,
189
+ vendorId,
190
+ commissionRateBps,
191
+ });
192
+ }
193
+ },
194
+ },
195
+
196
+ // ─── orders.beforeStatusChange ─────────────────────────────────────────
197
+ {
198
+ key: "orders.beforeStatusChange",
199
+ async handler(args: unknown) {
200
+ const { data } = args as { data: { orderId: string; newStatus: string } };
201
+ if (data.newStatus !== "fulfilled") return data;
202
+
203
+ const db = getDbFromHookArgs(args);
204
+ const related = await db.select().from(vendorSubOrders).where(eq(vendorSubOrders.orderId, data.orderId));
205
+ const allDelivered = related.every((subOrder: { status: string }) => subOrder.status === "delivered");
206
+
207
+ if (!allDelivered) {
208
+ throw new Error("Cannot fulfill parent order until all marketplace sub-orders are delivered.");
209
+ }
210
+
211
+ return data;
212
+ },
213
+ },
214
+ ];
215
+ }
package/src/index.ts ADDED
@@ -0,0 +1,124 @@
1
+ import { defineCommercePlugin } from "@unifiedcommerce/core";
2
+ import { MARKETPLACE_ANALYTICS_MODELS } from "./analytics-models";
3
+ import {
4
+ vendors, vendorEntities, vendorSubOrders, vendorPayouts,
5
+ vendorDocuments, commissionRules, vendorBalances,
6
+ disputes, vendorReviews, returnRequests,
7
+ rfqs, rfqResponses, contractPrices,
8
+ } from "./schema";
9
+ import { VendorService } from "./services/vendor";
10
+ import { SubOrderService } from "./services/sub-order";
11
+ import { CommissionService } from "./services/commission";
12
+ import { PayoutService } from "./services/payout";
13
+ import { DisputeService } from "./services/dispute";
14
+ import { ReturnService } from "./services/return";
15
+ import { ReviewService } from "./services/review";
16
+ import { RFQService } from "./services/rfq";
17
+ import { ContractPriceService } from "./services/contract-price";
18
+ import { buildHooks } from "./hooks";
19
+ import { buildMCPTools } from "./mcp-tools";
20
+ import { buildVendorRoutes } from "./routes/vendors";
21
+ import { buildVendorPortalRoutes } from "./routes/vendor-portal";
22
+ import { buildSubOrderRoutes } from "./routes/sub-orders";
23
+ import { buildCommissionRoutes } from "./routes/commission";
24
+ import { buildPayoutRoutes } from "./routes/payouts";
25
+ import { buildDisputesReturnsReviewsRoutes } from "./routes/disputes-returns-reviews";
26
+ import { buildB2BRoutes } from "./routes/b2b";
27
+ import type { MarketplacePluginOptions, Db } from "./types";
28
+
29
+ export type { MarketplacePluginOptions } from "./types";
30
+
31
+ function createServices(db: Db, options: MarketplacePluginOptions, kernelServices?: Record<string, unknown>) {
32
+ const vendor = new VendorService(db);
33
+ const commission = new CommissionService(db, options);
34
+ const payout = new PayoutService(db, options);
35
+ const dispute = new DisputeService(db, options);
36
+ const returnSvc = new ReturnService(db);
37
+ const review = new ReviewService(db, options);
38
+ const rfq = options.b2b?.rfq ? new RFQService(db) : undefined;
39
+ const contractPrice = options.b2b?.contractPricing ? new ContractPriceService(db) : undefined;
40
+
41
+ // Cancel callback: release inventory on parent order + reverse balance
42
+ const subOrder = new SubOrderService(db, async (sub) => {
43
+ // Release inventory for cancelled vendor's line items
44
+ const inventory = kernelServices?.inventory as
45
+ { release(input: Record<string, unknown>): Promise<unknown> } | undefined;
46
+ if (inventory?.release && sub.lineItems) {
47
+ for (const item of sub.lineItems as Array<{ entityId: string; quantity: number }>) {
48
+ await inventory.release({
49
+ entityId: item.entityId,
50
+ quantity: item.quantity,
51
+ orderId: sub.orderId,
52
+ performedBy: "marketplace",
53
+ });
54
+ }
55
+ }
56
+
57
+ // Reverse balance: debit the sale credit
58
+ if (sub.payoutAmount > 0) {
59
+ await payout.addLedgerEntry({
60
+ vendorId: sub.vendorId,
61
+ type: "refund_deduction",
62
+ amountCents: -sub.payoutAmount,
63
+ referenceType: "sub_order",
64
+ referenceId: sub.id,
65
+ description: `Cancelled sub-order ${sub.id.slice(0, 8)}`,
66
+ });
67
+ }
68
+ });
69
+
70
+ return { vendor, subOrder, commission, payout, dispute, return: returnSvc, review, rfq, contractPrice };
71
+ }
72
+
73
+ export function marketplacePlugin(options: MarketplacePluginOptions = {}) {
74
+ return defineCommercePlugin({
75
+ id: "marketplace",
76
+ version: "2.0.0",
77
+
78
+ schema: () => ({
79
+ vendors,
80
+ vendorEntities,
81
+ vendorSubOrders,
82
+ vendorPayouts,
83
+ vendorDocuments,
84
+ commissionRules,
85
+ vendorBalances,
86
+ disputes,
87
+ vendorReviews,
88
+ returnRequests,
89
+ rfqs,
90
+ rfqResponses,
91
+ contractPrices,
92
+ }),
93
+
94
+ hooks: () => buildHooks(options),
95
+
96
+ routes: (ctx) => {
97
+ const db = ctx.database.db as unknown as Db;
98
+ const services = db ? createServices(db, options, ctx.services) : null;
99
+
100
+ if (!services) return [];
101
+
102
+ return [
103
+ ...buildVendorRoutes(services, options),
104
+ ...buildVendorPortalRoutes(services),
105
+ ...buildSubOrderRoutes(services),
106
+ ...buildCommissionRoutes(services),
107
+ ...buildPayoutRoutes(services),
108
+ ...buildDisputesReturnsReviewsRoutes(services),
109
+ ...(options.b2b?.rfq || options.b2b?.contractPricing
110
+ ? buildB2BRoutes(services, options)
111
+ : []),
112
+ ];
113
+ },
114
+
115
+ mcpTools: (ctx) => {
116
+ const db = ctx.database.db as unknown as Db;
117
+ if (!db) return [];
118
+ const services = createServices(db, options, ctx.services);
119
+ return buildMCPTools(services, options);
120
+ },
121
+
122
+ analyticsModels: () => MARKETPLACE_ANALYTICS_MODELS,
123
+ });
124
+ }