@porulle/core 0.8.0 → 0.10.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 (162) hide show
  1. package/dist/auth/middleware.d.ts.map +1 -1
  2. package/dist/auth/middleware.js +26 -7
  3. package/dist/auth/setup.d.ts.map +1 -1
  4. package/dist/auth/setup.js +7 -5
  5. package/dist/config/defaults.d.ts.map +1 -1
  6. package/dist/config/defaults.js +3 -1
  7. package/dist/config/types.d.ts +13 -3
  8. package/dist/config/types.d.ts.map +1 -1
  9. package/dist/generated/plugin-manifest.d.ts +3 -0
  10. package/dist/generated/plugin-manifest.d.ts.map +1 -1
  11. package/dist/generated/plugin-manifest.js +1 -0
  12. package/dist/hooks/checkout-completion.d.ts.map +1 -1
  13. package/dist/hooks/checkout-completion.js +2 -2
  14. package/dist/hooks/checkout.d.ts.map +1 -1
  15. package/dist/hooks/checkout.js +1 -1
  16. package/dist/index.d.ts +5 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -0
  19. package/dist/interfaces/rest/router.d.ts.map +1 -1
  20. package/dist/interfaces/rest/router.js +2 -0
  21. package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -1
  22. package/dist/interfaces/rest/routes/admin/staff.js +37 -3
  23. package/dist/interfaces/rest/routes/checkout.d.ts +9 -0
  24. package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
  25. package/dist/interfaces/rest/routes/checkout.js +82 -22
  26. package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
  27. package/dist/interfaces/rest/routes/inventory.js +13 -0
  28. package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
  29. package/dist/interfaces/rest/routes/orders.js +13 -0
  30. package/dist/interfaces/rest/schemas/catalog.d.ts +391 -0
  31. package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
  32. package/dist/interfaces/rest/schemas/orders.js +2 -2
  33. package/dist/interfaces/rest/schemas/responses.d.ts +85 -0
  34. package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
  35. package/dist/interfaces/rest/schemas/search.d.ts +17 -0
  36. package/dist/interfaces/rest/schemas/search.d.ts.map +1 -1
  37. package/dist/kernel/database/scoped-db.d.ts +2 -1
  38. package/dist/kernel/database/scoped-db.d.ts.map +1 -1
  39. package/dist/kernel/database/scoped-db.js +74 -15
  40. package/dist/kernel/errors.d.ts +12 -0
  41. package/dist/kernel/errors.d.ts.map +1 -1
  42. package/dist/kernel/errors.js +16 -0
  43. package/dist/kernel/jobs/adapter.d.ts +27 -8
  44. package/dist/kernel/jobs/adapter.d.ts.map +1 -1
  45. package/dist/kernel/jobs/adapter.js +1 -2
  46. package/dist/kernel/jobs/drizzle-adapter.d.ts +13 -3
  47. package/dist/kernel/jobs/drizzle-adapter.d.ts.map +1 -1
  48. package/dist/kernel/jobs/drizzle-adapter.js +46 -5
  49. package/dist/kernel/jobs/runner.d.ts +2 -1
  50. package/dist/kernel/jobs/runner.d.ts.map +1 -1
  51. package/dist/kernel/jobs/runner.js +100 -15
  52. package/dist/kernel/jobs/types.d.ts +13 -0
  53. package/dist/kernel/jobs/types.d.ts.map +1 -1
  54. package/dist/kernel/plugin/manifest.d.ts +5 -0
  55. package/dist/kernel/plugin/manifest.d.ts.map +1 -1
  56. package/dist/kernel/plugin/manifest.js +9 -0
  57. package/dist/modules/analytics/drizzle-adapter.d.ts.map +1 -1
  58. package/dist/modules/analytics/drizzle-adapter.js +16 -3
  59. package/dist/modules/cart/service.d.ts.map +1 -1
  60. package/dist/modules/cart/service.js +3 -2
  61. package/dist/modules/catalog/entity-service.d.ts +1 -0
  62. package/dist/modules/catalog/entity-service.d.ts.map +1 -1
  63. package/dist/modules/catalog/entity-service.js +49 -3
  64. package/dist/modules/catalog/repository/index.d.ts +2 -2
  65. package/dist/modules/catalog/repository/index.d.ts.map +1 -1
  66. package/dist/modules/catalog/repository/index.js +9 -1
  67. package/dist/modules/catalog/schema.d.ts +51 -0
  68. package/dist/modules/catalog/schema.d.ts.map +1 -1
  69. package/dist/modules/catalog/schema.js +12 -1
  70. package/dist/modules/catalog/schemas.d.ts +3 -1
  71. package/dist/modules/catalog/schemas.d.ts.map +1 -1
  72. package/dist/modules/channels/adapter.d.ts +117 -0
  73. package/dist/modules/channels/adapter.d.ts.map +1 -0
  74. package/dist/modules/channels/adapter.js +3 -0
  75. package/dist/modules/customers/service.d.ts.map +1 -1
  76. package/dist/modules/customers/service.js +6 -0
  77. package/dist/modules/inventory/repository/index.js +4 -4
  78. package/dist/modules/inventory/service.d.ts.map +1 -1
  79. package/dist/modules/inventory/service.js +7 -3
  80. package/dist/modules/orders/schema.d.ts +17 -0
  81. package/dist/modules/orders/schema.d.ts.map +1 -1
  82. package/dist/modules/orders/schema.js +4 -0
  83. package/dist/modules/orders/service.d.ts +16 -2
  84. package/dist/modules/orders/service.d.ts.map +1 -1
  85. package/dist/modules/orders/service.js +226 -26
  86. package/dist/modules/orders/stale-order-cleanup.d.ts.map +1 -1
  87. package/dist/modules/orders/stale-order-cleanup.js +37 -16
  88. package/dist/modules/pricing/service.d.ts.map +1 -1
  89. package/dist/modules/pricing/service.js +5 -5
  90. package/dist/modules/promotions/repository/index.d.ts +1 -1
  91. package/dist/modules/promotions/repository/index.d.ts.map +1 -1
  92. package/dist/modules/promotions/repository/index.js +6 -3
  93. package/dist/modules/promotions/service.d.ts +1 -0
  94. package/dist/modules/promotions/service.d.ts.map +1 -1
  95. package/dist/modules/promotions/service.js +1 -1
  96. package/dist/modules/webhooks/repository/index.d.ts +1 -1
  97. package/dist/modules/webhooks/repository/index.d.ts.map +1 -1
  98. package/dist/modules/webhooks/repository/index.js +16 -10
  99. package/dist/runtime/kernel.d.ts.map +1 -1
  100. package/dist/runtime/kernel.js +14 -2
  101. package/dist/runtime/server.d.ts +1 -4
  102. package/dist/runtime/server.d.ts.map +1 -1
  103. package/dist/runtime/server.js +16 -34
  104. package/dist/test-utils/create-test-config.d.ts.map +1 -1
  105. package/dist/test-utils/create-test-config.js +2 -0
  106. package/dist/test-utils/order-test-helpers.d.ts +12 -0
  107. package/dist/test-utils/order-test-helpers.d.ts.map +1 -0
  108. package/dist/test-utils/order-test-helpers.js +18 -0
  109. package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
  110. package/dist/test-utils/rest-api-test-utils.js +1 -0
  111. package/dist/test-utils/test-actors.js +1 -1
  112. package/dist/testing.d.ts +1 -0
  113. package/dist/testing.d.ts.map +1 -1
  114. package/dist/testing.js +1 -0
  115. package/package.json +1 -1
  116. package/src/auth/middleware.ts +26 -7
  117. package/src/auth/setup.ts +10 -4
  118. package/src/config/defaults.ts +3 -1
  119. package/src/config/types.ts +16 -3
  120. package/src/generated/plugin-capabilities.d.ts +1 -0
  121. package/src/generated/plugin-manifest.ts +1 -0
  122. package/src/generated/plugin-repositories.d.ts +1 -0
  123. package/src/hooks/checkout-completion.ts +22 -16
  124. package/src/hooks/checkout.ts +11 -8
  125. package/src/index.ts +31 -1
  126. package/src/interfaces/rest/router.ts +2 -0
  127. package/src/interfaces/rest/routes/admin/staff.ts +45 -3
  128. package/src/interfaces/rest/routes/checkout.ts +94 -28
  129. package/src/interfaces/rest/routes/inventory.ts +14 -0
  130. package/src/interfaces/rest/routes/orders.ts +11 -0
  131. package/src/interfaces/rest/schemas/orders.ts +2 -2
  132. package/src/kernel/database/scoped-db.ts +83 -15
  133. package/src/kernel/errors.ts +20 -0
  134. package/src/kernel/jobs/adapter.ts +39 -8
  135. package/src/kernel/jobs/drizzle-adapter.ts +72 -9
  136. package/src/kernel/jobs/runner.ts +194 -72
  137. package/src/kernel/jobs/types.ts +25 -0
  138. package/src/kernel/plugin/manifest.ts +15 -0
  139. package/src/modules/analytics/drizzle-adapter.ts +19 -3
  140. package/src/modules/cart/service.ts +3 -2
  141. package/src/modules/catalog/entity-service.ts +37 -2
  142. package/src/modules/catalog/repository/index.ts +13 -3
  143. package/src/modules/catalog/schema.ts +33 -18
  144. package/src/modules/catalog/schemas.ts +3 -1
  145. package/src/modules/channels/adapter.ts +139 -0
  146. package/src/modules/customers/service.ts +6 -0
  147. package/src/modules/inventory/repository/index.ts +4 -4
  148. package/src/modules/inventory/service.ts +12 -2
  149. package/src/modules/orders/schema.ts +4 -0
  150. package/src/modules/orders/service.ts +404 -64
  151. package/src/modules/orders/stale-order-cleanup.ts +52 -27
  152. package/src/modules/pricing/service.ts +5 -6
  153. package/src/modules/promotions/repository/index.ts +6 -2
  154. package/src/modules/promotions/service.ts +2 -0
  155. package/src/modules/webhooks/repository/index.ts +16 -14
  156. package/src/runtime/kernel.ts +16 -2
  157. package/src/runtime/server.ts +22 -42
  158. package/src/test-utils/create-test-config.ts +2 -0
  159. package/src/test-utils/order-test-helpers.ts +26 -0
  160. package/src/test-utils/rest-api-test-utils.ts +1 -0
  161. package/src/test-utils/test-actors.ts +1 -1
  162. package/src/testing.ts +1 -0
@@ -1,4 +1,5 @@
1
1
  import type { AnyPgColumn } from "drizzle-orm/pg-core";
2
+ import { sql } from "drizzle-orm";
2
3
  import {
3
4
  boolean,
4
5
  index,
@@ -17,6 +18,8 @@ export const sellableEntities = pgTable(
17
18
  {
18
19
  id: uuid("id").defaultRandom().primaryKey(),
19
20
  organizationId: text("organization_id").notNull().references(() => organization.id, { onDelete: "cascade" }),
21
+ // Connected-store id when externally sourced; null means platform-native.
22
+ sourceStoreId: text("source_store_id"),
20
23
  type: text("type").notNull(),
21
24
  slug: text("slug").notNull(),
22
25
  status: text("status", {
@@ -179,24 +182,36 @@ export const optionValues = pgTable("option_values", {
179
182
  metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
180
183
  });
181
184
 
182
- export const variants = pgTable("variants", {
183
- id: uuid("id").defaultRandom().primaryKey(),
184
- entityId: uuid("entity_id")
185
- .references(() => sellableEntities.id, { onDelete: "cascade" })
186
- .notNull(),
187
- sku: text("sku").unique(),
188
- barcode: text("barcode"),
189
- // Overrides the entity's taxClass when set (issue #57).
190
- taxClass: text("tax_class"),
191
- status: text("status", { enum: ["active", "discontinued"] })
192
- .notNull()
193
- .default("active"),
194
- sortOrder: integer("sort_order").notNull().default(0),
195
- metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
196
- }, (table) => ({
197
- barcodeIdx: index("idx_variants_barcode").on(table.barcode),
198
- skuIdx: index("idx_variants_sku").on(table.sku),
199
- }));
185
+ export const variants = pgTable(
186
+ "variants",
187
+ {
188
+ id: uuid("id").defaultRandom().primaryKey(),
189
+ entityId: uuid("entity_id")
190
+ .references(() => sellableEntities.id, { onDelete: "cascade" })
191
+ .notNull(),
192
+ organizationId: text("organization_id").notNull().references(() => organization.id, { onDelete: "cascade" }),
193
+ sourceStoreId: text("source_store_id"),
194
+ sku: text("sku"),
195
+ barcode: text("barcode"),
196
+ // Overrides the entity's taxClass when set (issue #57).
197
+ taxClass: text("tax_class"),
198
+ status: text("status", { enum: ["active", "discontinued"] })
199
+ .notNull()
200
+ .default("active"),
201
+ sortOrder: integer("sort_order").notNull().default(0),
202
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
203
+ },
204
+ (table) => ({
205
+ barcodeIdx: index("idx_variants_barcode").on(table.barcode),
206
+ skuIdx: index("idx_variants_sku").on(table.sku),
207
+ nativeSkuUnique: uniqueIndex("variants_native_org_sku_unique")
208
+ .on(table.organizationId, table.sku)
209
+ .where(sql`${table.sourceStoreId} IS NULL AND ${table.sku} IS NOT NULL`),
210
+ sourceSkuUnique: uniqueIndex("variants_source_store_sku_unique")
211
+ .on(table.sourceStoreId, table.sku)
212
+ .where(sql`${table.sourceStoreId} IS NOT NULL AND ${table.sku} IS NOT NULL`),
213
+ }),
214
+ );
200
215
 
201
216
  export const variantOptionValues = pgTable("variant_option_values", {
202
217
  variantId: uuid("variant_id")
@@ -92,7 +92,9 @@ export const GenerateVariantsBodySchema = z
92
92
 
93
93
  // ─── Derived Input Types ─────────────────────────────────────────────────────
94
94
 
95
- export type CreateEntityInput = z.infer<typeof CreateEntityBodySchema>;
95
+ export type CreateEntityInput = z.infer<typeof CreateEntityBodySchema> & {
96
+ sourceStoreId?: string;
97
+ };
96
98
 
97
99
  export type UpdateEntityInput = z.infer<typeof UpdateEntityBodySchema>;
98
100
 
@@ -0,0 +1,139 @@
1
+ import type { Result } from "../../kernel/result.js";
2
+
3
+ export interface ChannelConnectorError {
4
+ code: string;
5
+ message: string;
6
+ retriable?: boolean;
7
+ }
8
+
9
+ export interface ChannelConnectorCapabilities {
10
+ readonly importCatalog: boolean;
11
+ readonly importInventory: boolean;
12
+ readonly pushOrder: boolean;
13
+ readonly receiveWebhooks: boolean;
14
+ readonly reserve?: boolean;
15
+ }
16
+
17
+ export interface ChannelStore {
18
+ id: string;
19
+ organizationId: string;
20
+ provider: string;
21
+ credentials: Record<string, unknown>;
22
+ storeDomain: string;
23
+ status: "connected" | "disconnected" | "error";
24
+ webhookSecret: string | null;
25
+ }
26
+
27
+ export interface ChannelCatalogVariant {
28
+ externalId: string;
29
+ sku?: string;
30
+ barcode?: string;
31
+ metadata?: Record<string, unknown>;
32
+ }
33
+
34
+ export interface ChannelCatalogItem {
35
+ externalId: string;
36
+ slug: string;
37
+ title: string;
38
+ description?: string;
39
+ variants: ChannelCatalogVariant[];
40
+ metadata?: Record<string, unknown>;
41
+ }
42
+
43
+ export interface ChannelCatalogPage {
44
+ items: ChannelCatalogItem[];
45
+ nextCursor?: string | null;
46
+ }
47
+
48
+ export interface ChannelInventoryLevel {
49
+ externalId: string;
50
+ available: number;
51
+ }
52
+
53
+ export interface ChannelOrderLine {
54
+ externalVariantId: string;
55
+ sku?: string;
56
+ title: string;
57
+ quantity: number;
58
+ unitPrice: number;
59
+ totalPrice: number;
60
+ }
61
+
62
+ export interface ChannelOrderSlice {
63
+ orderId: string;
64
+ currency: string;
65
+ grandTotal: number;
66
+ lines: ChannelOrderLine[];
67
+ customer: {
68
+ name: string;
69
+ email: string;
70
+ shippingAddress: Record<string, unknown>;
71
+ };
72
+ }
73
+
74
+ export interface ChannelPushOrderResult {
75
+ remoteOrderId: string;
76
+ remoteUrl?: string;
77
+ }
78
+
79
+ export interface ChannelOrderStatus {
80
+ status: "pending" | "confirmed" | "failed" | "cancelled" | "fulfilled";
81
+ }
82
+
83
+ export interface ChannelWebhookEvent {
84
+ id: string;
85
+ type: string;
86
+ data: unknown;
87
+ }
88
+
89
+ export interface ChannelReservation {
90
+ id: string;
91
+ expiresAt?: Date;
92
+ }
93
+
94
+ export interface ChannelRefundResult {
95
+ remoteRefundId: string;
96
+ status: string;
97
+ }
98
+
99
+ export interface ChannelConnector {
100
+ readonly providerId: string;
101
+ readonly capabilities: ChannelConnectorCapabilities;
102
+ buildAuthUrl?(params: {
103
+ storeDomain: string;
104
+ state: string;
105
+ redirectUri: string;
106
+ callbackUri: string;
107
+ scopes: string[];
108
+ }): Result<string, ChannelConnectorError>;
109
+ completeAuth?(
110
+ request: Request,
111
+ ctx: { storeDomain: string },
112
+ ): Promise<Result<{ credentials: Record<string, unknown>; storeDomain: string }, ChannelConnectorError>>;
113
+ importCatalog(store: ChannelStore, cursor?: string): Promise<Result<ChannelCatalogPage>>;
114
+ fetchInventory(store: ChannelStore, ids?: string[]): Promise<Result<ChannelInventoryLevel[]>>;
115
+ pushOrder(store: ChannelStore, slice: ChannelOrderSlice): Promise<Result<ChannelPushOrderResult, ChannelConnectorError>>;
116
+ fetchOrderStatus(store: ChannelStore, remoteId: string): Promise<Result<ChannelOrderStatus, ChannelConnectorError>>;
117
+ verifyWebhook(store: ChannelStore, request: Request): Promise<Result<ChannelWebhookEvent>>;
118
+ verifyAppWebhook?(
119
+ request: Request,
120
+ ): Promise<Result<{ topic: string; shopDomain: string; data: unknown }, ChannelConnectorError>>;
121
+ registerWebhooks?(
122
+ store: ChannelStore,
123
+ topics: string[],
124
+ callbackUrl: string,
125
+ ): Promise<Result<{ registered: number }, ChannelConnectorError>>;
126
+ reserve?(
127
+ store: ChannelStore,
128
+ lines: ChannelOrderLine[],
129
+ ): Promise<Result<ChannelReservation>>;
130
+ refundExecute(
131
+ store: ChannelStore,
132
+ slice: ChannelOrderSlice,
133
+ amount: number,
134
+ ): Promise<Result<ChannelRefundResult>>;
135
+ }
136
+
137
+ export function defineChannelConnector<T extends ChannelConnector>(connector: T): T {
138
+ return connector;
139
+ }
@@ -241,6 +241,12 @@ export class CustomerService {
241
241
  ctx?: TxContext,
242
242
  options?: { replaceMetadata?: boolean },
243
243
  ): Promise<Result<Customer>> {
244
+ try {
245
+ assertPermission(actor ?? null, "customers:update");
246
+ } catch (error) {
247
+ return Err(toCommerceError(error));
248
+ }
249
+
244
250
  const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
245
251
  const existing = await this.repo.findById(orgId, id, ctx);
246
252
  if (!existing) return Err(new CommerceNotFoundError("Customer not found."));
@@ -1,5 +1,5 @@
1
1
  import { eq, and, inArray, isNull, sql } from "drizzle-orm";
2
- import { CommerceValidationError } from "../../../kernel/errors.js";
2
+ import { CommerceValidationError, INVENTORY_RECORD_NOT_FOUND_MESSAGE } from "../../../kernel/errors.js";
3
3
  import type { TxContext } from "../../../kernel/database/tx-context.js";
4
4
  import type {
5
5
  DrizzleDatabase,
@@ -495,7 +495,7 @@ export class InventoryRepository {
495
495
  if (!level) {
496
496
  return {
497
497
  ok: false,
498
- reason: "No inventory record found for this entity.",
498
+ reason: INVENTORY_RECORD_NOT_FOUND_MESSAGE,
499
499
  };
500
500
  }
501
501
 
@@ -550,7 +550,7 @@ export class InventoryRepository {
550
550
  if (!level) {
551
551
  return {
552
552
  ok: false,
553
- reason: "No inventory record found for this entity.",
553
+ reason: INVENTORY_RECORD_NOT_FOUND_MESSAGE,
554
554
  };
555
555
  }
556
556
 
@@ -600,7 +600,7 @@ export class InventoryRepository {
600
600
  if (!level) {
601
601
  return {
602
602
  ok: false,
603
- reason: "No inventory record found for this entity.",
603
+ reason: INVENTORY_RECORD_NOT_FOUND_MESSAGE,
604
604
  };
605
605
  }
606
606
 
@@ -3,8 +3,10 @@ import { assertPermission } from "../../auth/permissions.js";
3
3
  import type { Actor } from "../../auth/types.js";
4
4
  import type { CommerceConfig } from "../../config/types.js";
5
5
  import {
6
+ CommerceInventoryRecordNotFoundError,
6
7
  CommerceNotFoundError,
7
8
  CommerceValidationError,
9
+ INVENTORY_RECORD_NOT_FOUND_MESSAGE,
8
10
  toCommerceError,
9
11
  } from "../../kernel/errors.js";
10
12
  import { runAfterHooks } from "../../kernel/hooks/executor.js";
@@ -232,7 +234,11 @@ export class InventoryService {
232
234
  );
233
235
 
234
236
  if (!reserveResult.ok) {
235
- return Err(new CommerceValidationError(reserveResult.reason));
237
+ return Err(
238
+ reserveResult.reason === INVENTORY_RECORD_NOT_FOUND_MESSAGE
239
+ ? new CommerceInventoryRecordNotFoundError()
240
+ : new CommerceValidationError(reserveResult.reason),
241
+ );
236
242
  }
237
243
 
238
244
  await this.repo.createMovement(
@@ -281,7 +287,11 @@ export class InventoryService {
281
287
  );
282
288
 
283
289
  if (!releaseResult.ok) {
284
- return Err(new CommerceValidationError(releaseResult.reason));
290
+ return Err(
291
+ releaseResult.reason === INVENTORY_RECORD_NOT_FOUND_MESSAGE
292
+ ? new CommerceInventoryRecordNotFoundError()
293
+ : new CommerceValidationError(releaseResult.reason),
294
+ );
285
295
  }
286
296
 
287
297
  await this.repo.createMovement(
@@ -50,6 +50,10 @@ export const orderLineItems = pgTable("order_line_items", {
50
50
  quantity: integer("quantity").notNull(),
51
51
  unitPrice: integer("unit_price").notNull(),
52
52
  totalPrice: integer("total_price").notNull(),
53
+ // Price provenance (matches Medusa's `is_custom_price`): true when a staff
54
+ // actor with `orders:manage` supplied an explicit override price rather than
55
+ // the server-derived catalog price. Audit + reporting signal.
56
+ isCustomPrice: boolean("is_custom_price").notNull().default(false),
53
57
  taxAmount: integer("tax_amount").notNull().default(0),
54
58
  discountAmount: integer("discount_amount").notNull().default(0),
55
59
  // Units already refunded on this line (issue #52) — line-level refund REST