@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,120 @@
1
+ import { eq, and, desc, lte, gte, isNull, or } from "drizzle-orm";
2
+ import { commissionRules, vendors } from "../schema";
3
+ import type { Db, MarketplacePluginOptions } from "../types";
4
+
5
+ export class CommissionService {
6
+ constructor(
7
+ private db: Db,
8
+ private options: MarketplacePluginOptions,
9
+ ) {}
10
+
11
+ async createRule(data: {
12
+ name: string;
13
+ type: string;
14
+ rateBps: number;
15
+ categorySlug?: string;
16
+ vendorId?: string;
17
+ vendorTier?: string;
18
+ minVolumeCents?: number;
19
+ maxVolumeCents?: number;
20
+ validFrom?: Date | undefined;
21
+ validUntil?: Date | undefined;
22
+ priority?: number;
23
+ }) {
24
+ const [rule] = await this.db.insert(commissionRules).values(data).returning();
25
+ return rule;
26
+ }
27
+
28
+ async updateRule(id: string, data: Record<string, unknown>) {
29
+ const [updated] = await this.db.update(commissionRules).set(data)
30
+ .where(eq(commissionRules.id, id)).returning();
31
+ return updated ?? null;
32
+ }
33
+
34
+ async deleteRule(id: string) {
35
+ await this.db.delete(commissionRules).where(eq(commissionRules.id, id));
36
+ }
37
+
38
+ async listRules() {
39
+ return this.db.select().from(commissionRules).orderBy(desc(commissionRules.priority));
40
+ }
41
+
42
+ /**
43
+ * Resolve the effective commission rate for a given vendor+category+volume.
44
+ * Priority order per RFC §5.2:
45
+ * 1. Vendor-specific + category-specific
46
+ * 2. Category-specific (any vendor)
47
+ * 3. Vendor tier rule
48
+ * 4. Volume tier rule
49
+ * 5. Promotional rule
50
+ * 6. Vendor-level flat rate
51
+ * 7. Plugin default
52
+ */
53
+ async resolveRate(vendorId: string, categorySlug?: string, volumeCents?: number): Promise<number> {
54
+ const now = new Date();
55
+ const rules = await this.db.select().from(commissionRules)
56
+ .where(eq(commissionRules.isActive, true))
57
+ .orderBy(desc(commissionRules.priority));
58
+
59
+ // Load vendor for tier and flat rate fallback
60
+ const [vendor] = await this.db.select().from(vendors).where(eq(vendors.id, vendorId));
61
+
62
+ for (const rule of rules) {
63
+ // Check time validity
64
+ if (rule.validFrom && rule.validFrom > now) continue;
65
+ if (rule.validUntil && rule.validUntil < now) continue;
66
+
67
+ // 1. Vendor-specific + category-specific
68
+ if (rule.type === "category" && rule.vendorId === vendorId && rule.categorySlug === categorySlug) {
69
+ return rule.rateBps;
70
+ }
71
+ }
72
+
73
+ for (const rule of rules) {
74
+ if (rule.validFrom && rule.validFrom > now) continue;
75
+ if (rule.validUntil && rule.validUntil < now) continue;
76
+
77
+ // 2. Category-specific (any vendor)
78
+ if (rule.type === "category" && !rule.vendorId && rule.categorySlug === categorySlug) {
79
+ return rule.rateBps;
80
+ }
81
+ }
82
+
83
+ for (const rule of rules) {
84
+ if (rule.validFrom && rule.validFrom > now) continue;
85
+ if (rule.validUntil && rule.validUntil < now) continue;
86
+
87
+ // 3. Vendor tier
88
+ if (rule.type === "vendor_tier" && vendor && rule.vendorTier === vendor.tier) {
89
+ return rule.rateBps;
90
+ }
91
+
92
+ // 4. Volume tier
93
+ if (rule.type === "volume_tier" && volumeCents != null) {
94
+ const min = rule.minVolumeCents ?? 0;
95
+ const max = rule.maxVolumeCents ?? Number.MAX_SAFE_INTEGER;
96
+ if (volumeCents >= min && volumeCents <= max) {
97
+ return rule.rateBps;
98
+ }
99
+ }
100
+
101
+ // 5. Promotional
102
+ if (rule.type === "promotional") {
103
+ if (!rule.vendorId || rule.vendorId === vendorId) {
104
+ return rule.rateBps;
105
+ }
106
+ }
107
+ }
108
+
109
+ // 6. Vendor flat rate
110
+ if (vendor) return vendor.commissionRateBps;
111
+
112
+ // 7. Plugin default
113
+ return this.options.defaultCommissionRateBps ?? 1000;
114
+ }
115
+
116
+ async previewRate(vendorId: string, categorySlug?: string, volumeCents?: number) {
117
+ const rateBps = await this.resolveRate(vendorId, categorySlug, volumeCents);
118
+ return { rateBps, ratePercent: rateBps / 100 };
119
+ }
120
+ }
@@ -0,0 +1,80 @@
1
+ import { eq, and, desc, lte, gte, isNull, or } from "drizzle-orm";
2
+ import { contractPrices } from "../schema";
3
+ import type { Db } from "../types";
4
+
5
+ export class ContractPriceService {
6
+ constructor(private db: Db) {}
7
+
8
+ async create(data: {
9
+ vendorId: string;
10
+ buyerId: string;
11
+ entityId: string;
12
+ variantId?: string;
13
+ priceCents: number;
14
+ minQuantity?: number;
15
+ currency?: string;
16
+ validFrom?: Date | undefined;
17
+ validUntil?: Date | undefined;
18
+ }) {
19
+ const [price] = await this.db.insert(contractPrices).values(data).returning();
20
+ return price;
21
+ }
22
+
23
+ async update(id: string, data: Record<string, unknown>) {
24
+ const [updated] = await this.db.update(contractPrices).set(data)
25
+ .where(eq(contractPrices.id, id)).returning();
26
+ return updated ?? null;
27
+ }
28
+
29
+ async delete(id: string) {
30
+ await this.db.delete(contractPrices).where(eq(contractPrices.id, id));
31
+ }
32
+
33
+ async list(filters?: { vendorId?: string; buyerId?: string }) {
34
+ let query = this.db.select().from(contractPrices).$dynamic();
35
+ const conditions = [];
36
+ if (filters?.vendorId) conditions.push(eq(contractPrices.vendorId, filters.vendorId));
37
+ if (filters?.buyerId) conditions.push(eq(contractPrices.buyerId, filters.buyerId));
38
+ if (conditions.length > 0) {
39
+ query = query.where(conditions.length === 1 ? conditions[0]! : and(...conditions));
40
+ }
41
+ return query.orderBy(desc(contractPrices.createdAt));
42
+ }
43
+
44
+ /**
45
+ * Resolve the best contract price for a buyer+vendor+entity+quantity.
46
+ * Returns null if no matching contract exists.
47
+ */
48
+ async resolvePrice(
49
+ vendorId: string,
50
+ buyerId: string,
51
+ entityId: string,
52
+ variantId: string | null,
53
+ quantity: number,
54
+ ): Promise<number | null> {
55
+ const now = new Date();
56
+ const all = await this.db.select().from(contractPrices)
57
+ .where(
58
+ and(
59
+ eq(contractPrices.vendorId, vendorId),
60
+ eq(contractPrices.buyerId, buyerId),
61
+ eq(contractPrices.entityId, entityId),
62
+ ),
63
+ )
64
+ .orderBy(desc(contractPrices.priceCents));
65
+
66
+ for (const cp of all) {
67
+ // Check variant match
68
+ if (variantId != null && cp.variantId != null && cp.variantId !== variantId) continue;
69
+ // Check quantity
70
+ if (quantity < (cp.minQuantity ?? 1)) continue;
71
+ // Check time validity
72
+ if (cp.validFrom && cp.validFrom > now) continue;
73
+ if (cp.validUntil && cp.validUntil < now) continue;
74
+
75
+ return cp.priceCents;
76
+ }
77
+
78
+ return null;
79
+ }
80
+ }
@@ -0,0 +1,92 @@
1
+ import { eq, and, desc } from "drizzle-orm";
2
+ import { disputes } from "../schema";
3
+ import type { Db, DisputeStatus, DisputeResolution, MarketplacePluginOptions } from "../types";
4
+
5
+ export class DisputeService {
6
+ constructor(
7
+ private db: Db,
8
+ private options: MarketplacePluginOptions,
9
+ ) {}
10
+
11
+ async open(data: {
12
+ subOrderId: string;
13
+ openedBy: string;
14
+ reason: string;
15
+ description?: string;
16
+ }) {
17
+ const deadlineDays = this.options.vendorResponseDeadlineDays ?? 3;
18
+ const deadline = new Date();
19
+ deadline.setDate(deadline.getDate() + deadlineDays);
20
+
21
+ const [dispute] = await this.db.insert(disputes).values({
22
+ subOrderId: data.subOrderId,
23
+ openedBy: data.openedBy,
24
+ reason: data.reason,
25
+ description: data.description,
26
+ status: "vendor_response_pending",
27
+ deadlineAt: deadline,
28
+ }).returning();
29
+ return dispute;
30
+ }
31
+
32
+ async getById(id: string) {
33
+ const [dispute] = await this.db.select().from(disputes).where(eq(disputes.id, id));
34
+ return dispute ?? null;
35
+ }
36
+
37
+ async list(filters?: { status?: string; subOrderId?: string }) {
38
+ let query = this.db.select().from(disputes).$dynamic();
39
+ const conditions = [];
40
+ if (filters?.status) conditions.push(eq(disputes.status, filters.status));
41
+ if (filters?.subOrderId) conditions.push(eq(disputes.subOrderId, filters.subOrderId));
42
+ if (conditions.length > 0) {
43
+ query = query.where(conditions.length === 1 ? conditions[0]! : and(...conditions));
44
+ }
45
+ return query.orderBy(desc(disputes.openedAt));
46
+ }
47
+
48
+ async respond(id: string, data: { party: string; note: string; url?: string }) {
49
+ const dispute = await this.getById(id);
50
+ if (!dispute) throw new Error("Dispute not found.");
51
+
52
+ const evidence = [...(dispute.evidence as Array<{ party: string; type: string; url?: string; note?: string; at: string }> ?? [])];
53
+ const entry: { party: string; type: string; note: string; at: string; url?: string } = {
54
+ party: data.party,
55
+ type: "response",
56
+ note: data.note,
57
+ at: new Date().toISOString(),
58
+ };
59
+ if (data.url) entry.url = data.url;
60
+ evidence.push(entry);
61
+
62
+ const [updated] = await this.db.update(disputes).set({
63
+ evidence,
64
+ status: "platform_review",
65
+ }).where(eq(disputes.id, id)).returning();
66
+ return updated;
67
+ }
68
+
69
+ async escalate(id: string) {
70
+ const [updated] = await this.db.update(disputes).set({
71
+ status: "escalated" as DisputeStatus,
72
+ }).where(eq(disputes.id, id)).returning();
73
+ return updated ?? null;
74
+ }
75
+
76
+ async resolve(id: string, data: {
77
+ resolution: DisputeResolution;
78
+ notes?: string;
79
+ refundAmountCents?: number;
80
+ resolvedBy: string;
81
+ }) {
82
+ const [updated] = await this.db.update(disputes).set({
83
+ status: "resolved" as DisputeStatus,
84
+ resolution: data.resolution,
85
+ resolutionNotes: data.notes,
86
+ refundAmountCents: data.refundAmountCents,
87
+ resolvedBy: data.resolvedBy,
88
+ resolvedAt: new Date(),
89
+ }).where(eq(disputes.id, id)).returning();
90
+ return updated ?? null;
91
+ }
92
+ }
@@ -0,0 +1,154 @@
1
+ import { eq, and, desc, sql, lte } from "drizzle-orm";
2
+ import { vendorPayouts, vendorBalances, vendors, vendorSubOrders } from "../schema";
3
+ import type { Db, BalanceEntryType, MarketplacePluginOptions } from "../types";
4
+
5
+ export class PayoutService {
6
+ constructor(
7
+ private db: Db,
8
+ private options: MarketplacePluginOptions,
9
+ ) {}
10
+
11
+ // ─── Balance Ledger ────────────────────────────────────────────────────────
12
+
13
+ async addLedgerEntry(data: {
14
+ vendorId: string;
15
+ type: BalanceEntryType;
16
+ amountCents: number;
17
+ referenceType?: string;
18
+ referenceId?: string;
19
+ description?: string;
20
+ }) {
21
+ // Get current balance
22
+ const balance = await this.getBalance(data.vendorId);
23
+ const newBalance = balance + data.amountCents;
24
+
25
+ const [entry] = await this.db.insert(vendorBalances).values({
26
+ vendorId: data.vendorId,
27
+ type: data.type,
28
+ amountCents: data.amountCents,
29
+ runningBalanceCents: newBalance,
30
+ referenceType: data.referenceType,
31
+ referenceId: data.referenceId,
32
+ description: data.description,
33
+ }).returning();
34
+ return entry;
35
+ }
36
+
37
+ async getBalance(vendorId: string): Promise<number> {
38
+ const rows = await this.db.select({
39
+ balance: sql<number>`COALESCE((
40
+ SELECT running_balance_cents FROM marketplace_vendor_balances
41
+ WHERE vendor_id = ${vendorId}
42
+ ORDER BY created_at DESC LIMIT 1
43
+ ), 0)`,
44
+ }).from(vendors).where(eq(vendors.id, vendorId));
45
+ return rows[0]?.balance ?? 0;
46
+ }
47
+
48
+ async getLedger(vendorId: string, limit = 50) {
49
+ return this.db.select().from(vendorBalances)
50
+ .where(eq(vendorBalances.vendorId, vendorId))
51
+ .orderBy(desc(vendorBalances.createdAt))
52
+ .limit(limit);
53
+ }
54
+
55
+ // ─── Payouts ───────────────────────────────────────────────────────────────
56
+
57
+ async getPayoutById(id: string) {
58
+ const [payout] = await this.db.select().from(vendorPayouts).where(eq(vendorPayouts.id, id));
59
+ return payout ?? null;
60
+ }
61
+
62
+ async listPayouts(filters?: { vendorId?: string; status?: string }) {
63
+ let query = this.db.select().from(vendorPayouts).$dynamic();
64
+ const conditions = [];
65
+ if (filters?.vendorId) conditions.push(eq(vendorPayouts.vendorId, filters.vendorId));
66
+ if (filters?.status) conditions.push(eq(vendorPayouts.status, filters.status));
67
+ if (conditions.length > 0) {
68
+ query = query.where(conditions.length === 1 ? conditions[0]! : and(...conditions));
69
+ }
70
+ return query.orderBy(desc(vendorPayouts.createdAt));
71
+ }
72
+
73
+ /**
74
+ * Run payout cycle for eligible vendors.
75
+ * Per RFC §5.5:
76
+ * 1. Find vendors with matching payout_schedule
77
+ * 2. Check balance >= payout_minimum_cents
78
+ * 3. Only include deliveries older than holdback_days
79
+ * 4. Calculate deductions (refunds, adjustments)
80
+ * 5. Create payout record + balance ledger debit
81
+ */
82
+ async runPayoutCycle(): Promise<Array<{ vendorId: string; payoutId: string; netAmount: number }>> {
83
+ const allVendors = await this.db.select().from(vendors)
84
+ .where(eq(vendors.status, "approved"));
85
+
86
+ const results: Array<{ vendorId: string; payoutId: string; netAmount: number }> = [];
87
+
88
+ for (const vendor of allVendors) {
89
+ const balance = await this.getBalance(vendor.id);
90
+ const minimum = vendor.payoutMinimumCents ?? this.options.defaultPayoutMinimumCents ?? 5000;
91
+
92
+ if (balance < minimum) continue;
93
+
94
+ const grossAmount = balance;
95
+ const deductions: Array<{ type: string; amount: number; reference?: string }> = [];
96
+ const netAmount = grossAmount - deductions.reduce((sum, d) => sum + d.amount, 0);
97
+
98
+ if (netAmount <= 0) continue;
99
+
100
+ const [payout] = await this.db.insert(vendorPayouts).values({
101
+ vendorId: vendor.id,
102
+ amount: netAmount,
103
+ status: "processing",
104
+ grossAmount,
105
+ deductions,
106
+ netAmount,
107
+ periodEnd: new Date(),
108
+ }).returning();
109
+
110
+ if (!payout) continue;
111
+
112
+ // Debit vendor balance
113
+ await this.addLedgerEntry({
114
+ vendorId: vendor.id,
115
+ type: "payout",
116
+ amountCents: -netAmount,
117
+ referenceType: "payout",
118
+ referenceId: payout.id,
119
+ description: `Payout #${payout.id.slice(0, 8)}`,
120
+ });
121
+
122
+ // Mark as completed (in a real system, this would wait for bank transfer confirmation)
123
+ await this.db.update(vendorPayouts).set({
124
+ status: "completed",
125
+ processedAt: new Date(),
126
+ }).where(eq(vendorPayouts.id, payout.id));
127
+
128
+ results.push({ vendorId: vendor.id, payoutId: payout.id, netAmount });
129
+ }
130
+
131
+ return results;
132
+ }
133
+
134
+ async retryPayout(payoutId: string) {
135
+ const payout = await this.getPayoutById(payoutId);
136
+ if (!payout) throw new Error("Payout not found.");
137
+ if (payout.status !== "failed") throw new Error("Only failed payouts can be retried.");
138
+
139
+ const [updated] = await this.db.update(vendorPayouts).set({
140
+ status: "processing",
141
+ retryCount: (payout.retryCount ?? 0) + 1,
142
+ failedAt: null,
143
+ failureReason: null,
144
+ }).where(eq(vendorPayouts.id, payoutId)).returning();
145
+
146
+ // Mark completed (simplified — real implementation calls payment provider)
147
+ await this.db.update(vendorPayouts).set({
148
+ status: "completed",
149
+ processedAt: new Date(),
150
+ }).where(eq(vendorPayouts.id, payoutId));
151
+
152
+ return updated;
153
+ }
154
+ }
@@ -0,0 +1,92 @@
1
+ import { eq, and, desc } from "drizzle-orm";
2
+ import { returnRequests, vendorSubOrders } from "../schema";
3
+ import type { Db, ReturnStatus } from "../types";
4
+
5
+ export class ReturnService {
6
+ constructor(private db: Db) {}
7
+
8
+ async request(data: {
9
+ subOrderId: string;
10
+ customerId?: string;
11
+ reason: string;
12
+ description?: string;
13
+ lineItems?: Array<{ entityId: string; quantity: number; reason?: string }>;
14
+ }) {
15
+ const [req] = await this.db.insert(returnRequests).values({
16
+ subOrderId: data.subOrderId,
17
+ customerId: data.customerId,
18
+ reason: data.reason,
19
+ description: data.description,
20
+ lineItems: data.lineItems,
21
+ }).returning();
22
+ return req;
23
+ }
24
+
25
+ async getById(id: string) {
26
+ const [req] = await this.db.select().from(returnRequests).where(eq(returnRequests.id, id));
27
+ return req ?? null;
28
+ }
29
+
30
+ async list(filters?: { subOrderId?: string; status?: string }) {
31
+ let query = this.db.select().from(returnRequests).$dynamic();
32
+ const conditions = [];
33
+ if (filters?.subOrderId) conditions.push(eq(returnRequests.subOrderId, filters.subOrderId));
34
+ if (filters?.status) conditions.push(eq(returnRequests.status, filters.status));
35
+ if (conditions.length > 0) {
36
+ query = query.where(conditions.length === 1 ? conditions[0]! : and(...conditions));
37
+ }
38
+ return query.orderBy(desc(returnRequests.requestedAt));
39
+ }
40
+
41
+ async listByVendor(vendorId: string) {
42
+ const subOrders = await this.db.select().from(vendorSubOrders)
43
+ .where(eq(vendorSubOrders.vendorId, vendorId));
44
+ const subOrderIds = subOrders.map((s: { id: string }) => s.id);
45
+ if (subOrderIds.length === 0) return [];
46
+
47
+ const all = await this.db.select().from(returnRequests)
48
+ .orderBy(desc(returnRequests.requestedAt));
49
+ return all.filter((r: { subOrderId: string }) => subOrderIds.includes(r.subOrderId));
50
+ }
51
+
52
+ async vendorApprove(id: string, refundAmountCents?: number) {
53
+ const [updated] = await this.db.update(returnRequests).set({
54
+ status: "vendor_approved" as ReturnStatus,
55
+ refundAmountCents,
56
+ }).where(eq(returnRequests.id, id)).returning();
57
+ return updated ?? null;
58
+ }
59
+
60
+ async vendorReject(id: string, notes?: string) {
61
+ const [updated] = await this.db.update(returnRequests).set({
62
+ status: "vendor_rejected" as ReturnStatus,
63
+ vendorNotes: notes,
64
+ resolvedAt: new Date(),
65
+ }).where(eq(returnRequests.id, id)).returning();
66
+ return updated ?? null;
67
+ }
68
+
69
+ async shipBack(id: string, trackingNumber: string) {
70
+ const [updated] = await this.db.update(returnRequests).set({
71
+ status: "shipped_back" as ReturnStatus,
72
+ trackingNumber,
73
+ }).where(eq(returnRequests.id, id)).returning();
74
+ return updated ?? null;
75
+ }
76
+
77
+ async receive(id: string) {
78
+ const [updated] = await this.db.update(returnRequests).set({
79
+ status: "received" as ReturnStatus,
80
+ }).where(eq(returnRequests.id, id)).returning();
81
+ return updated ?? null;
82
+ }
83
+
84
+ async refund(id: string, amountCents: number) {
85
+ const [updated] = await this.db.update(returnRequests).set({
86
+ status: "refunded" as ReturnStatus,
87
+ refundAmountCents: amountCents,
88
+ resolvedAt: new Date(),
89
+ }).where(eq(returnRequests.id, id)).returning();
90
+ return updated ?? null;
91
+ }
92
+ }
@@ -0,0 +1,76 @@
1
+ import { eq, and, desc, sql, avg } from "drizzle-orm";
2
+ import { vendorReviews } from "../schema";
3
+ import type { Db, ReviewStatus, MarketplacePluginOptions } from "../types";
4
+
5
+ export class ReviewService {
6
+ constructor(
7
+ private db: Db,
8
+ private options: MarketplacePluginOptions,
9
+ ) {}
10
+
11
+ async create(data: {
12
+ vendorId: string;
13
+ customerId?: string;
14
+ orderId?: string;
15
+ rating: number;
16
+ title?: string;
17
+ body?: string;
18
+ }) {
19
+ if (data.rating < 1 || data.rating > 5) {
20
+ throw new Error("Rating must be between 1 and 5.");
21
+ }
22
+
23
+ const status: ReviewStatus = this.options.reviewModerationEnabled ? "pending" : "published";
24
+
25
+ const [review] = await this.db.insert(vendorReviews).values({
26
+ vendorId: data.vendorId,
27
+ customerId: data.customerId,
28
+ orderId: data.orderId,
29
+ rating: data.rating,
30
+ title: data.title,
31
+ body: data.body,
32
+ status,
33
+ }).returning();
34
+ return review;
35
+ }
36
+
37
+ async getForVendor(vendorId: string, includeUnpublished = false) {
38
+ let query = this.db.select().from(vendorReviews).$dynamic();
39
+ if (includeUnpublished) {
40
+ query = query.where(eq(vendorReviews.vendorId, vendorId));
41
+ } else {
42
+ query = query.where(
43
+ and(eq(vendorReviews.vendorId, vendorId), eq(vendorReviews.status, "published")),
44
+ );
45
+ }
46
+ return query.orderBy(desc(vendorReviews.createdAt));
47
+ }
48
+
49
+ async getAggregateRating(vendorId: string): Promise<{ average: number; count: number }> {
50
+ const rows = await this.db.select({
51
+ avg: sql<number>`COALESCE(AVG(${vendorReviews.rating}), 0)`,
52
+ count: sql<number>`COUNT(*)`,
53
+ }).from(vendorReviews)
54
+ .where(and(eq(vendorReviews.vendorId, vendorId), eq(vendorReviews.status, "published")));
55
+
56
+ const row = rows[0];
57
+ return {
58
+ average: Math.round((Number(row?.avg) || 0) * 10) / 10,
59
+ count: Number(row?.count) || 0,
60
+ };
61
+ }
62
+
63
+ async respond(reviewId: string, response: string) {
64
+ const [updated] = await this.db.update(vendorReviews).set({
65
+ vendorResponse: response,
66
+ vendorRespondedAt: new Date(),
67
+ }).where(eq(vendorReviews.id, reviewId)).returning();
68
+ return updated ?? null;
69
+ }
70
+
71
+ async moderate(reviewId: string, status: ReviewStatus) {
72
+ const [updated] = await this.db.update(vendorReviews).set({ status })
73
+ .where(eq(vendorReviews.id, reviewId)).returning();
74
+ return updated ?? null;
75
+ }
76
+ }
@@ -0,0 +1,82 @@
1
+ import { eq, and, desc } from "drizzle-orm";
2
+ import { rfqs, rfqResponses } from "../schema";
3
+ import type { Db, RFQStatus, RFQResponseStatus } from "../types";
4
+
5
+ export class RFQService {
6
+ constructor(private db: Db) {}
7
+
8
+ async create(data: {
9
+ buyerId?: string;
10
+ title: string;
11
+ description?: string;
12
+ categorySlug?: string;
13
+ quantity?: number;
14
+ budgetCents?: number;
15
+ currency?: string;
16
+ deadlineAt?: Date | undefined;
17
+ metadata?: Record<string, unknown>;
18
+ }) {
19
+ const [rfq] = await this.db.insert(rfqs).values(data).returning();
20
+ return rfq;
21
+ }
22
+
23
+ async getById(id: string) {
24
+ const [rfq] = await this.db.select().from(rfqs).where(eq(rfqs.id, id));
25
+ return rfq ?? null;
26
+ }
27
+
28
+ async list(filters?: { status?: string; categorySlug?: string }) {
29
+ let query = this.db.select().from(rfqs).$dynamic();
30
+ const conditions = [];
31
+ if (filters?.status) conditions.push(eq(rfqs.status, filters.status));
32
+ if (filters?.categorySlug) conditions.push(eq(rfqs.categorySlug, filters.categorySlug));
33
+ if (conditions.length > 0) {
34
+ query = query.where(conditions.length === 1 ? conditions[0]! : and(...conditions));
35
+ }
36
+ return query.orderBy(desc(rfqs.createdAt));
37
+ }
38
+
39
+ async respond(rfqId: string, data: {
40
+ vendorId: string;
41
+ unitPriceCents: number;
42
+ totalPriceCents: number;
43
+ leadTimeDays?: number;
44
+ notes?: string;
45
+ }) {
46
+ const [response] = await this.db.insert(rfqResponses).values({
47
+ rfqId,
48
+ ...data,
49
+ }).returning();
50
+ return response;
51
+ }
52
+
53
+ async getResponses(rfqId: string) {
54
+ return this.db.select().from(rfqResponses)
55
+ .where(eq(rfqResponses.rfqId, rfqId))
56
+ .orderBy(desc(rfqResponses.createdAt));
57
+ }
58
+
59
+ async award(rfqId: string, vendorId: string) {
60
+ // Mark RFQ as awarded
61
+ const [updated] = await this.db.update(rfqs).set({
62
+ status: "awarded" as RFQStatus,
63
+ awardedVendorId: vendorId,
64
+ }).where(eq(rfqs.id, rfqId)).returning();
65
+
66
+ // Mark winning response as accepted, others as rejected
67
+ const responses = await this.getResponses(rfqId);
68
+ for (const r of responses) {
69
+ const status: RFQResponseStatus = r.vendorId === vendorId ? "accepted" : "rejected";
70
+ await this.db.update(rfqResponses).set({ status }).where(eq(rfqResponses.id, r.id));
71
+ }
72
+
73
+ return updated ?? null;
74
+ }
75
+
76
+ async close(rfqId: string) {
77
+ const [updated] = await this.db.update(rfqs).set({
78
+ status: "closed" as RFQStatus,
79
+ }).where(eq(rfqs.id, rfqId)).returning();
80
+ return updated ?? null;
81
+ }
82
+ }