@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
package/dist/hooks.js ADDED
@@ -0,0 +1,187 @@
1
+ import { eq } from "@porulle/core/drizzle";
2
+ import { vendors, vendorEntities, vendorSubOrders } from "./schema.js";
3
+ import { CommissionService } from "./services/commission.js";
4
+ import { PayoutService } from "./services/payout.js";
5
+ function getDbFromHookArgs(args) {
6
+ const a = args;
7
+ return a.context.services.database.db;
8
+ }
9
+ export function buildHooks(options) {
10
+ return [
11
+ // ─── catalog.beforeCreate ──────────────────────────────────────────────
12
+ {
13
+ key: "catalog.beforeCreate",
14
+ async handler(args) {
15
+ const { data } = args;
16
+ const metadata = data?.metadata;
17
+ const vendorId = metadata?.vendorId;
18
+ if (!vendorId)
19
+ return data;
20
+ const db = getDbFromHookArgs(args);
21
+ const [vendor] = await db.select().from(vendors).where(eq(vendors.id, String(vendorId)));
22
+ if (!vendor)
23
+ throw new Error("Vendor not found.");
24
+ if (vendor.status === "suspended")
25
+ throw new Error("Vendor is suspended and cannot create listings.");
26
+ if (vendor.status !== "approved")
27
+ throw new Error("Vendor must be approved before creating marketplace listings.");
28
+ // Check approved categories if set
29
+ const approvedCats = vendor.approvedCategories;
30
+ if (approvedCats && Array.isArray(approvedCats)) {
31
+ const entityType = data.type;
32
+ // approvedCategories holds category slugs; entityType may match
33
+ // This is a basic check; real implementations would check category assignment
34
+ }
35
+ return data;
36
+ },
37
+ },
38
+ // ─── catalog.afterCreate ───────────────────────────────────────────────
39
+ {
40
+ key: "catalog.afterCreate",
41
+ async handler(args) {
42
+ const { result } = args;
43
+ const vendorId = result?.metadata?.vendorId;
44
+ if (!vendorId)
45
+ return;
46
+ const db = getDbFromHookArgs(args);
47
+ await db.insert(vendorEntities).values({
48
+ vendorId: String(vendorId),
49
+ entityId: result.id,
50
+ });
51
+ },
52
+ },
53
+ // ─── catalog.beforeList ────────────────────────────────────────────────
54
+ {
55
+ key: "catalog.beforeList",
56
+ async handler(args) {
57
+ const { data, context: hookContext } = args;
58
+ hookContext.context.marketplaceVendorScope = hookContext.actor?.vendorId ?? null;
59
+ return data;
60
+ },
61
+ },
62
+ // ─── catalog.afterList ─────────────────────────────────────────────────
63
+ {
64
+ key: "catalog.afterList",
65
+ async handler(args) {
66
+ const { result, context: hookContext } = args;
67
+ const vendorScope = hookContext.context.marketplaceVendorScope;
68
+ if (!vendorScope || !Array.isArray(result?.items))
69
+ return;
70
+ const db = getDbFromHookArgs(args);
71
+ const links = await db.select().from(vendorEntities).where(eq(vendorEntities.vendorId, vendorScope));
72
+ const linkedEntityIds = new Set(links.map((l) => l.entityId));
73
+ result.items = result.items.filter((item) => linkedEntityIds.has(item.id));
74
+ },
75
+ },
76
+ // ─── catalog.afterRead ─────────────────────────────────────────────────
77
+ {
78
+ key: "catalog.afterRead",
79
+ async handler(args) {
80
+ const { result } = args;
81
+ const db = getDbFromHookArgs(args);
82
+ const [link] = await db.select().from(vendorEntities).where(eq(vendorEntities.entityId, result.id));
83
+ if (!link)
84
+ return;
85
+ const [vendor] = await db.select().from(vendors).where(eq(vendors.id, link.vendorId));
86
+ if (!vendor)
87
+ return;
88
+ result.marketplace = {
89
+ vendor: {
90
+ id: vendor.id,
91
+ name: vendor.name,
92
+ slug: vendor.slug,
93
+ status: vendor.status,
94
+ tier: vendor.tier,
95
+ logoUrl: vendor.logoUrl,
96
+ },
97
+ };
98
+ },
99
+ },
100
+ // ─── orders.afterCreate ────────────────────────────────────────────────
101
+ {
102
+ key: "orders.afterCreate",
103
+ async handler(args) {
104
+ const { result, context: hookContext } = args;
105
+ const db = getDbFromHookArgs(args);
106
+ const commissionService = new CommissionService(db, options);
107
+ const payoutService = new PayoutService(db, options);
108
+ const grouped = new Map();
109
+ for (const lineItem of result.lineItems ?? []) {
110
+ const [link] = await db.select().from(vendorEntities).where(eq(vendorEntities.entityId, lineItem.entityId));
111
+ if (!link)
112
+ continue;
113
+ const [vendor] = await db.select().from(vendors).where(eq(vendors.id, link.vendorId));
114
+ if (!vendor)
115
+ continue;
116
+ const existing = grouped.get(vendor.id) ?? [];
117
+ existing.push({
118
+ entityId: lineItem.entityId,
119
+ quantity: lineItem.quantity,
120
+ totalPrice: lineItem.totalPrice,
121
+ });
122
+ grouped.set(vendor.id, existing);
123
+ }
124
+ for (const [vendorId, lineItems] of grouped.entries()) {
125
+ const subtotal = lineItems.reduce((sum, item) => sum + item.totalPrice, 0);
126
+ // Resolve commission via rules engine
127
+ const commissionRateBps = await commissionService.resolveRate(vendorId);
128
+ const commissionAmount = Math.round((subtotal * commissionRateBps) / 10000);
129
+ const payoutAmount = subtotal - commissionAmount;
130
+ const [subOrder] = await db.insert(vendorSubOrders).values({
131
+ orderId: result.id,
132
+ vendorId,
133
+ status: "pending",
134
+ subtotal,
135
+ commissionAmount,
136
+ payoutAmount,
137
+ notified: true,
138
+ lineItems,
139
+ metadata: {},
140
+ }).returning();
141
+ if (!subOrder)
142
+ continue;
143
+ // Credit vendor balance with full subtotal
144
+ await payoutService.addLedgerEntry({
145
+ vendorId,
146
+ type: "sale",
147
+ amountCents: subtotal,
148
+ referenceType: "sub_order",
149
+ referenceId: subOrder.id,
150
+ description: `Sale from order ${result.id.slice(0, 8)}`,
151
+ });
152
+ // Record commission as separate entry
153
+ await payoutService.addLedgerEntry({
154
+ vendorId,
155
+ type: "commission",
156
+ amountCents: -commissionAmount,
157
+ referenceType: "sub_order",
158
+ referenceId: subOrder.id,
159
+ description: `Commission (${commissionRateBps}bps) on order ${result.id.slice(0, 8)}`,
160
+ });
161
+ hookContext.logger.info("marketplace_sub_order_created", {
162
+ orderId: result.id,
163
+ subOrderId: subOrder.id,
164
+ vendorId,
165
+ commissionRateBps,
166
+ });
167
+ }
168
+ },
169
+ },
170
+ // ─── orders.beforeStatusChange ─────────────────────────────────────────
171
+ {
172
+ key: "orders.beforeStatusChange",
173
+ async handler(args) {
174
+ const { data } = args;
175
+ if (data.newStatus !== "fulfilled")
176
+ return data;
177
+ const db = getDbFromHookArgs(args);
178
+ const related = await db.select().from(vendorSubOrders).where(eq(vendorSubOrders.orderId, data.orderId));
179
+ const allDelivered = related.every((subOrder) => subOrder.status === "delivered");
180
+ if (!allDelivered) {
181
+ throw new Error("Cannot fulfill parent order until all marketplace sub-orders are delivered.");
182
+ }
183
+ return data;
184
+ },
185
+ },
186
+ ];
187
+ }
@@ -0,0 +1,4 @@
1
+ import type { MarketplacePluginOptions } from "./types.js";
2
+ export type { MarketplacePluginOptions } from "./types.js";
3
+ export declare function marketplacePlugin(options?: MarketplacePluginOptions): import("@porulle/core").CommercePlugin;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,wBAAwB,EAAM,MAAM,YAAY,CAAC;AAE/D,YAAY,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AA4C3D,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,wBAA6B,0CA4CvE"}
package/dist/index.js ADDED
@@ -0,0 +1,97 @@
1
+ import { defineCommercePlugin } from "@porulle/core";
2
+ import { MARKETPLACE_ANALYTICS_MODELS } from "./analytics-models.js";
3
+ import { vendors, vendorEntities, vendorSubOrders, vendorPayouts, vendorDocuments, commissionRules, vendorBalances, disputes, vendorReviews, returnRequests, rfqs, rfqResponses, contractPrices, } from "./schema.js";
4
+ import { VendorService } from "./services/vendor.js";
5
+ import { SubOrderService } from "./services/sub-order.js";
6
+ import { CommissionService } from "./services/commission.js";
7
+ import { PayoutService } from "./services/payout.js";
8
+ import { DisputeService } from "./services/dispute.js";
9
+ import { ReturnService } from "./services/return.js";
10
+ import { ReviewService } from "./services/review.js";
11
+ import { RFQService } from "./services/rfq.js";
12
+ import { ContractPriceService } from "./services/contract-price.js";
13
+ import { buildHooks } from "./hooks.js";
14
+ import { buildVendorRoutes } from "./routes/vendors.js";
15
+ import { buildVendorPortalRoutes } from "./routes/vendor-portal.js";
16
+ import { buildSubOrderRoutes } from "./routes/sub-orders.js";
17
+ import { buildCommissionRoutes } from "./routes/commission.js";
18
+ import { buildPayoutRoutes } from "./routes/payouts.js";
19
+ import { buildDisputesReturnsReviewsRoutes } from "./routes/disputes-returns-reviews.js";
20
+ import { buildB2BRoutes } from "./routes/b2b.js";
21
+ function createServices(db, options, kernelServices) {
22
+ const vendor = new VendorService(db);
23
+ const commission = new CommissionService(db, options);
24
+ const payout = new PayoutService(db, options);
25
+ const dispute = new DisputeService(db, options);
26
+ const returnSvc = new ReturnService(db);
27
+ const review = new ReviewService(db, options);
28
+ const rfq = options.b2b?.rfq ? new RFQService(db) : undefined;
29
+ const contractPrice = options.b2b?.contractPricing ? new ContractPriceService(db) : undefined;
30
+ // Cancel callback: release inventory on parent order + reverse balance
31
+ const subOrder = new SubOrderService(db, async (sub) => {
32
+ // Release inventory for cancelled vendor's line items
33
+ const inventory = kernelServices?.inventory;
34
+ if (inventory?.release && sub.lineItems) {
35
+ for (const item of sub.lineItems) {
36
+ await inventory.release({
37
+ entityId: item.entityId,
38
+ quantity: item.quantity,
39
+ orderId: sub.orderId,
40
+ performedBy: "marketplace",
41
+ });
42
+ }
43
+ }
44
+ // Reverse balance: debit the sale credit
45
+ if (sub.payoutAmount > 0) {
46
+ await payout.addLedgerEntry({
47
+ vendorId: sub.vendorId,
48
+ type: "refund_deduction",
49
+ amountCents: -sub.payoutAmount,
50
+ referenceType: "sub_order",
51
+ referenceId: sub.id,
52
+ description: `Cancelled sub-order ${sub.id.slice(0, 8)}`,
53
+ });
54
+ }
55
+ });
56
+ return { vendor, subOrder, commission, payout, dispute, return: returnSvc, review, rfq, contractPrice };
57
+ }
58
+ export function marketplacePlugin(options = {}) {
59
+ return defineCommercePlugin({
60
+ id: "marketplace",
61
+ version: "2.0.0",
62
+ schema: () => ({
63
+ vendors,
64
+ vendorEntities,
65
+ vendorSubOrders,
66
+ vendorPayouts,
67
+ vendorDocuments,
68
+ commissionRules,
69
+ vendorBalances,
70
+ disputes,
71
+ vendorReviews,
72
+ returnRequests,
73
+ rfqs,
74
+ rfqResponses,
75
+ contractPrices,
76
+ }),
77
+ hooks: () => buildHooks(options),
78
+ routes: (ctx) => {
79
+ const db = ctx.database.db;
80
+ const services = db ? createServices(db, options, ctx.services) : null;
81
+ if (!services)
82
+ return [];
83
+ return [
84
+ ...buildVendorRoutes(services, options),
85
+ ...buildVendorPortalRoutes(services),
86
+ ...buildSubOrderRoutes(services),
87
+ ...buildCommissionRoutes(services),
88
+ ...buildPayoutRoutes(services),
89
+ ...buildDisputesReturnsReviewsRoutes(services),
90
+ ...(options.b2b?.rfq || options.b2b?.contractPricing
91
+ ? buildB2BRoutes(services, options)
92
+ : []),
93
+ ];
94
+ },
95
+ analyticsModels: () => MARKETPLACE_ANALYTICS_MODELS,
96
+ });
97
+ }
@@ -0,0 +1,9 @@
1
+ import type { PluginRouteRegistration } from "@porulle/core";
2
+ import type { RFQService } from "../services/rfq.js";
3
+ import type { ContractPriceService } from "../services/contract-price.js";
4
+ import type { MarketplacePluginOptions } from "../types.js";
5
+ export declare function buildB2BRoutes(services: {
6
+ rfq?: RFQService | undefined;
7
+ contractPrice?: ContractPriceService | undefined;
8
+ }, options: MarketplacePluginOptions): PluginRouteRegistration[];
9
+ //# sourceMappingURL=b2b.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"b2b.d.ts","sourceRoot":"","sources":["../../src/routes/b2b.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAE7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAU5D,wBAAgB,cAAc,CAAC,QAAQ,EAAE;IACvC,GAAG,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IAC7B,aAAa,CAAC,EAAE,oBAAoB,GAAG,SAAS,CAAC;CAClD,EAAE,OAAO,EAAE,wBAAwB,GAAG,uBAAuB,EAAE,CAgK/D"}
@@ -0,0 +1,156 @@
1
+ import { router } from "@porulle/core";
2
+ import { CreateRFQBodySchema, RespondRFQBodySchema, AwardRFQBodySchema, CreateContractPriceBodySchema, UpdateContractPriceBodySchema, } from "../schemas/b2b.js";
3
+ import { stripUndefined } from "./util.js";
4
+ export function buildB2BRoutes(services, options) {
5
+ const allRoutes = [];
6
+ // ═══════════════════════════════════════════════════════════════════════════
7
+ // RFQ (Request for Quote)
8
+ // ═══════════════════════════════════════════════════════════════════════════
9
+ if (services.rfq) {
10
+ const rfqSvc = services.rfq;
11
+ const rfq = router("Marketplace - B2B", "/marketplace/rfq");
12
+ rfq.post("/")
13
+ .summary("Create a Request for Quote")
14
+ .auth()
15
+ .input(CreateRFQBodySchema)
16
+ .handler(async ({ input }) => {
17
+ const body = input;
18
+ return rfqSvc.create(stripUndefined({
19
+ buyerId: body.buyerId,
20
+ title: body.title,
21
+ description: body.description,
22
+ categorySlug: body.categorySlug,
23
+ quantity: body.quantity,
24
+ budgetCents: body.budgetCents,
25
+ currency: body.currency,
26
+ deadlineAt: body.deadlineAt ? new Date(body.deadlineAt) : undefined,
27
+ metadata: body.metadata,
28
+ }));
29
+ });
30
+ rfq.get("/")
31
+ .summary("List Requests for Quote")
32
+ .auth()
33
+ .handler(async ({ query }) => {
34
+ return rfqSvc.list(stripUndefined({
35
+ status: query.status,
36
+ categorySlug: query.categorySlug,
37
+ }));
38
+ });
39
+ rfq.get("/{id}")
40
+ .summary("Get RFQ detail")
41
+ .auth()
42
+ .handler(async ({ params }) => {
43
+ const item = await rfqSvc.getById(params.id);
44
+ if (!item)
45
+ throw new Error("RFQ not found");
46
+ const responses = await rfqSvc.getResponses(item.id);
47
+ return { ...item, responses };
48
+ });
49
+ rfq.post("/{id}/respond")
50
+ .summary("Submit a vendor response to an RFQ")
51
+ .auth()
52
+ .input(RespondRFQBodySchema)
53
+ .handler(async ({ params, input }) => {
54
+ const body = input;
55
+ const item = await rfqSvc.getById(params.id);
56
+ if (!item)
57
+ throw new Error("RFQ not found");
58
+ return rfqSvc.respond(item.id, stripUndefined({
59
+ vendorId: body.vendorId,
60
+ unitPriceCents: body.unitPriceCents,
61
+ totalPriceCents: body.totalPriceCents,
62
+ leadTimeDays: body.leadTimeDays,
63
+ notes: body.notes,
64
+ }));
65
+ });
66
+ rfq.post("/{id}/award")
67
+ .summary("Award an RFQ to a vendor")
68
+ .auth()
69
+ .permission("marketplace:admin")
70
+ .input(AwardRFQBodySchema)
71
+ .handler(async ({ params, input }) => {
72
+ const body = input;
73
+ const updated = await rfqSvc.award(params.id, body.vendorId);
74
+ if (!updated)
75
+ throw new Error("RFQ not found");
76
+ return updated;
77
+ });
78
+ rfq.post("/{id}/close")
79
+ .summary("Close an RFQ")
80
+ .auth()
81
+ .permission("marketplace:admin")
82
+ .handler(async ({ params }) => {
83
+ const updated = await rfqSvc.close(params.id);
84
+ if (!updated)
85
+ throw new Error("RFQ not found");
86
+ return updated;
87
+ });
88
+ allRoutes.push(...rfq.routes());
89
+ }
90
+ // ═══════════════════════════════════════════════════════════════════════════
91
+ // CONTRACT PRICES
92
+ // ═══════════════════════════════════════════════════════════════════════════
93
+ if (services.contractPrice) {
94
+ const cpSvc = services.contractPrice;
95
+ const cp = router("Marketplace - B2B", "/marketplace/contract-prices");
96
+ cp.get("/")
97
+ .summary("List contract prices")
98
+ .auth()
99
+ .handler(async ({ query }) => {
100
+ return cpSvc.list(stripUndefined({
101
+ vendorId: query.vendorId,
102
+ buyerId: query.buyerId,
103
+ }));
104
+ });
105
+ cp.post("/")
106
+ .summary("Create a contract price")
107
+ .auth()
108
+ .permission("marketplace:admin")
109
+ .input(CreateContractPriceBodySchema)
110
+ .handler(async ({ input }) => {
111
+ const body = input;
112
+ return cpSvc.create(stripUndefined({
113
+ vendorId: body.vendorId,
114
+ buyerId: body.buyerId,
115
+ entityId: body.entityId,
116
+ variantId: body.variantId,
117
+ priceCents: body.priceCents,
118
+ minQuantity: body.minQuantity,
119
+ currency: body.currency,
120
+ validFrom: body.validFrom ? new Date(body.validFrom) : undefined,
121
+ validUntil: body.validUntil ? new Date(body.validUntil) : undefined,
122
+ }));
123
+ });
124
+ cp.patch("/{id}")
125
+ .summary("Update a contract price")
126
+ .auth()
127
+ .permission("marketplace:admin")
128
+ .input(UpdateContractPriceBodySchema)
129
+ .handler(async ({ params, input }) => {
130
+ const body = input;
131
+ const updateData = {};
132
+ if (body.priceCents !== undefined)
133
+ updateData.priceCents = body.priceCents;
134
+ if (body.minQuantity !== undefined)
135
+ updateData.minQuantity = body.minQuantity;
136
+ if (body.validFrom !== undefined)
137
+ updateData.validFrom = body.validFrom ? new Date(body.validFrom) : null;
138
+ if (body.validUntil !== undefined)
139
+ updateData.validUntil = body.validUntil ? new Date(body.validUntil) : null;
140
+ const updated = await cpSvc.update(params.id, updateData);
141
+ if (!updated)
142
+ throw new Error("Contract price not found");
143
+ return updated;
144
+ });
145
+ cp.delete("/{id}")
146
+ .summary("Delete a contract price")
147
+ .auth()
148
+ .permission("marketplace:admin")
149
+ .handler(async ({ params }) => {
150
+ await cpSvc.delete(params.id);
151
+ return { deleted: true };
152
+ });
153
+ allRoutes.push(...cp.routes());
154
+ }
155
+ return allRoutes;
156
+ }
@@ -0,0 +1,6 @@
1
+ import type { PluginRouteRegistration } from "@porulle/core";
2
+ import type { CommissionService } from "../services/commission.js";
3
+ export declare function buildCommissionRoutes(services: {
4
+ commission: CommissionService;
5
+ }): PluginRouteRegistration[];
6
+ //# sourceMappingURL=commission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commission.d.ts","sourceRoot":"","sources":["../../src/routes/commission.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAE7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAQnE,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE;IAC9C,UAAU,EAAE,iBAAiB,CAAC;CAC/B,GAAG,uBAAuB,EAAE,CAiF5B"}
@@ -0,0 +1,85 @@
1
+ import { router } from "@porulle/core";
2
+ import { CreateCommissionRuleBodySchema, UpdateCommissionRuleBodySchema, PreviewCommissionBodySchema, } from "../schemas/commission.js";
3
+ import { stripUndefined } from "./util.js";
4
+ export function buildCommissionRoutes(services) {
5
+ const r = router("Marketplace - Commission", "/marketplace/commission-rules");
6
+ // ─── List commission rules ─────────────────────────────────────────────────
7
+ r.get("/")
8
+ .summary("List all commission rules")
9
+ .permission("marketplace:admin")
10
+ .handler(async () => {
11
+ return services.commission.listRules();
12
+ });
13
+ // ─── Create commission rule ────────────────────────────────────────────────
14
+ r.post("/")
15
+ .summary("Create a commission rule")
16
+ .permission("marketplace:admin")
17
+ .input(CreateCommissionRuleBodySchema)
18
+ .handler(async ({ input }) => {
19
+ const body = input;
20
+ return services.commission.createRule(stripUndefined({
21
+ name: body.name,
22
+ type: body.type,
23
+ rateBps: body.rateBps,
24
+ categorySlug: body.categorySlug,
25
+ vendorId: body.vendorId,
26
+ vendorTier: body.vendorTier,
27
+ minVolumeCents: body.minVolumeCents,
28
+ maxVolumeCents: body.maxVolumeCents,
29
+ validFrom: body.validFrom ? new Date(body.validFrom) : undefined,
30
+ validUntil: body.validUntil ? new Date(body.validUntil) : undefined,
31
+ priority: body.priority,
32
+ }));
33
+ });
34
+ // ─── Update commission rule ────────────────────────────────────────────────
35
+ r.patch("/{id}")
36
+ .summary("Update a commission rule")
37
+ .permission("marketplace:admin")
38
+ .input(UpdateCommissionRuleBodySchema)
39
+ .handler(async ({ params, input }) => {
40
+ const body = input;
41
+ const updateData = {};
42
+ if (body.name !== undefined)
43
+ updateData.name = body.name;
44
+ if (body.rateBps !== undefined)
45
+ updateData.rateBps = body.rateBps;
46
+ if (body.categorySlug !== undefined)
47
+ updateData.categorySlug = body.categorySlug;
48
+ if (body.vendorTier !== undefined)
49
+ updateData.vendorTier = body.vendorTier;
50
+ if (body.minVolumeCents !== undefined)
51
+ updateData.minVolumeCents = body.minVolumeCents;
52
+ if (body.maxVolumeCents !== undefined)
53
+ updateData.maxVolumeCents = body.maxVolumeCents;
54
+ if (body.validFrom !== undefined)
55
+ updateData.validFrom = body.validFrom ? new Date(body.validFrom) : null;
56
+ if (body.validUntil !== undefined)
57
+ updateData.validUntil = body.validUntil ? new Date(body.validUntil) : null;
58
+ if (body.priority !== undefined)
59
+ updateData.priority = body.priority;
60
+ if (body.isActive !== undefined)
61
+ updateData.isActive = body.isActive;
62
+ const updated = await services.commission.updateRule(params.id, updateData);
63
+ if (!updated)
64
+ throw new Error("Commission rule not found");
65
+ return updated;
66
+ });
67
+ // ─── Delete commission rule ────────────────────────────────────────────────
68
+ r.delete("/{id}")
69
+ .summary("Delete a commission rule")
70
+ .permission("marketplace:admin")
71
+ .handler(async ({ params }) => {
72
+ await services.commission.deleteRule(params.id);
73
+ return { deleted: true };
74
+ });
75
+ // ─── Preview commission rate ───────────────────────────────────────────────
76
+ r.post("/preview")
77
+ .summary("Preview the commission rate for a vendor")
78
+ .permission("marketplace:admin")
79
+ .input(PreviewCommissionBodySchema)
80
+ .handler(async ({ input }) => {
81
+ const body = input;
82
+ return services.commission.previewRate(body.vendorId, body.categorySlug, body.volumeCents);
83
+ });
84
+ return r.routes();
85
+ }
@@ -0,0 +1,10 @@
1
+ import type { PluginRouteRegistration } from "@porulle/core";
2
+ import type { DisputeService } from "../services/dispute.js";
3
+ import type { ReturnService } from "../services/return.js";
4
+ import type { ReviewService } from "../services/review.js";
5
+ export declare function buildDisputesReturnsReviewsRoutes(services: {
6
+ dispute: DisputeService;
7
+ return: ReturnService;
8
+ review: ReviewService;
9
+ }): PluginRouteRegistration[];
10
+ //# sourceMappingURL=disputes-returns-reviews.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disputes-returns-reviews.d.ts","sourceRoot":"","sources":["../../src/routes/disputes-returns-reviews.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAa3D,wBAAgB,iCAAiC,CAAC,QAAQ,EAAE;IAC1D,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,aAAa,CAAC;CACvB,GAAG,uBAAuB,EAAE,CA0L5B"}