@open-mercato/core 0.4.5-develop-3ce83a8b24 → 0.4.5-develop-4849712ccb

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 (163) hide show
  1. package/dist/generated/entities/catalog_product/index.js +16 -0
  2. package/dist/generated/entities/catalog_product/index.js.map +2 -2
  3. package/dist/generated/entities/catalog_product_unit_conversion/index.js +27 -0
  4. package/dist/generated/entities/catalog_product_unit_conversion/index.js.map +7 -0
  5. package/dist/generated/entities/sales_credit_memo_line/index.js +7 -1
  6. package/dist/generated/entities/sales_credit_memo_line/index.js.map +2 -2
  7. package/dist/generated/entities/sales_invoice_line/index.js +7 -1
  8. package/dist/generated/entities/sales_invoice_line/index.js.map +2 -2
  9. package/dist/generated/entities/sales_order_line/index.js +6 -0
  10. package/dist/generated/entities/sales_order_line/index.js.map +2 -2
  11. package/dist/generated/entities/sales_quote_line/index.js +6 -0
  12. package/dist/generated/entities/sales_quote_line/index.js.map +2 -2
  13. package/dist/generated/entities.ids.generated.js +1 -0
  14. package/dist/generated/entities.ids.generated.js.map +2 -2
  15. package/dist/generated/entity-fields-registry.js +2 -0
  16. package/dist/generated/entity-fields-registry.js.map +2 -2
  17. package/dist/modules/catalog/api/prices/route.js +123 -8
  18. package/dist/modules/catalog/api/prices/route.js.map +2 -2
  19. package/dist/modules/catalog/api/product-unit-conversions/route.js +194 -0
  20. package/dist/modules/catalog/api/product-unit-conversions/route.js.map +7 -0
  21. package/dist/modules/catalog/api/products/route.js +351 -201
  22. package/dist/modules/catalog/api/products/route.js.map +2 -2
  23. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +1267 -497
  24. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  25. package/dist/modules/catalog/backend/catalog/products/create/page.js +733 -210
  26. package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
  27. package/dist/modules/catalog/commands/index.js +1 -0
  28. package/dist/modules/catalog/commands/index.js.map +2 -2
  29. package/dist/modules/catalog/commands/productUnitConversions.js +503 -0
  30. package/dist/modules/catalog/commands/productUnitConversions.js.map +7 -0
  31. package/dist/modules/catalog/commands/products.js +355 -73
  32. package/dist/modules/catalog/commands/products.js.map +2 -2
  33. package/dist/modules/catalog/commands/shared.js +18 -4
  34. package/dist/modules/catalog/commands/shared.js.map +2 -2
  35. package/dist/modules/catalog/components/products/ProductUomSection.js +591 -0
  36. package/dist/modules/catalog/components/products/ProductUomSection.js.map +7 -0
  37. package/dist/modules/catalog/components/products/productForm.js +66 -5
  38. package/dist/modules/catalog/components/products/productForm.js.map +2 -2
  39. package/dist/modules/catalog/components/products/productFormUtils.js +68 -0
  40. package/dist/modules/catalog/components/products/productFormUtils.js.map +7 -0
  41. package/dist/modules/catalog/data/entities.js +86 -0
  42. package/dist/modules/catalog/data/entities.js.map +2 -2
  43. package/dist/modules/catalog/data/validators.js +65 -3
  44. package/dist/modules/catalog/data/validators.js.map +2 -2
  45. package/dist/modules/catalog/events.js +3 -0
  46. package/dist/modules/catalog/events.js.map +2 -2
  47. package/dist/modules/catalog/lib/unitCodes.js +7 -0
  48. package/dist/modules/catalog/lib/unitCodes.js.map +7 -0
  49. package/dist/modules/catalog/lib/unitResolution.js +53 -0
  50. package/dist/modules/catalog/lib/unitResolution.js.map +7 -0
  51. package/dist/modules/catalog/migrations/Migration20260218225422.js +19 -0
  52. package/dist/modules/catalog/migrations/Migration20260218225422.js.map +7 -0
  53. package/dist/modules/catalog/migrations/Migration20260219084500.js +27 -0
  54. package/dist/modules/catalog/migrations/Migration20260219084500.js.map +7 -0
  55. package/dist/modules/catalog/search.js +69 -1
  56. package/dist/modules/catalog/search.js.map +2 -2
  57. package/dist/modules/catalog/seed/examples.js +91 -42
  58. package/dist/modules/catalog/seed/examples.js.map +2 -2
  59. package/dist/modules/dashboards/seed/analytics.js +3 -0
  60. package/dist/modules/dashboards/seed/analytics.js.map +2 -2
  61. package/dist/modules/sales/api/order-lines/route.js +98 -15
  62. package/dist/modules/sales/api/order-lines/route.js.map +2 -2
  63. package/dist/modules/sales/api/quote-lines/route.js +101 -14
  64. package/dist/modules/sales/api/quote-lines/route.js.map +2 -2
  65. package/dist/modules/sales/api/quotes/public/[token]/route.js +87 -12
  66. package/dist/modules/sales/api/quotes/public/[token]/route.js.map +2 -2
  67. package/dist/modules/sales/commands/documents.js +1424 -260
  68. package/dist/modules/sales/commands/documents.js.map +3 -3
  69. package/dist/modules/sales/commands/shared.js +6 -2
  70. package/dist/modules/sales/commands/shared.js.map +2 -2
  71. package/dist/modules/sales/components/documents/ItemsSection.js +216 -86
  72. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  73. package/dist/modules/sales/components/documents/LineItemDialog.js +913 -241
  74. package/dist/modules/sales/components/documents/LineItemDialog.js.map +3 -3
  75. package/dist/modules/sales/components/documents/ShipmentsSection.js +15 -3
  76. package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
  77. package/dist/modules/sales/data/entities.js +59 -3
  78. package/dist/modules/sales/data/entities.js.map +2 -2
  79. package/dist/modules/sales/data/validators.js +35 -0
  80. package/dist/modules/sales/data/validators.js.map +2 -2
  81. package/dist/modules/sales/frontend/quote/[token]/page.js +15 -1
  82. package/dist/modules/sales/frontend/quote/[token]/page.js.map +2 -2
  83. package/dist/modules/sales/migrations/Migration20260218225423.js +31 -0
  84. package/dist/modules/sales/migrations/Migration20260218225423.js.map +7 -0
  85. package/dist/modules/sales/migrations/Migration20260219084501.js +71 -0
  86. package/dist/modules/sales/migrations/Migration20260219084501.js.map +7 -0
  87. package/dist/modules/sales/search.js +28 -0
  88. package/dist/modules/sales/search.js.map +2 -2
  89. package/dist/modules/sales/seed/examples.js +14 -1
  90. package/dist/modules/sales/seed/examples.js.map +2 -2
  91. package/dist/modules/sales/widgets/injection/document-history/widget.client.js +1 -1
  92. package/dist/modules/sales/widgets/injection/document-history/widget.client.js.map +2 -2
  93. package/generated/entities/catalog_product/index.ts +8 -0
  94. package/generated/entities/catalog_product_unit_conversion/index.ts +12 -0
  95. package/generated/entities/sales_credit_memo_line/index.ts +3 -0
  96. package/generated/entities/sales_invoice_line/index.ts +3 -0
  97. package/generated/entities/sales_order_line/index.ts +3 -0
  98. package/generated/entities/sales_quote_line/index.ts +3 -0
  99. package/generated/entities.ids.generated.ts +1 -0
  100. package/generated/entity-fields-registry.ts +2 -0
  101. package/package.json +2 -2
  102. package/src/modules/auth/i18n/de.json +1 -1
  103. package/src/modules/auth/i18n/en.json +1 -1
  104. package/src/modules/auth/i18n/es.json +1 -1
  105. package/src/modules/auth/i18n/pl.json +1 -1
  106. package/src/modules/catalog/api/prices/route.ts +213 -81
  107. package/src/modules/catalog/api/product-unit-conversions/route.ts +195 -0
  108. package/src/modules/catalog/api/products/route.ts +638 -402
  109. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +2085 -1072
  110. package/src/modules/catalog/backend/catalog/products/create/page.tsx +1288 -593
  111. package/src/modules/catalog/commands/index.ts +1 -0
  112. package/src/modules/catalog/commands/productUnitConversions.ts +626 -0
  113. package/src/modules/catalog/commands/products.ts +1151 -693
  114. package/src/modules/catalog/commands/shared.ts +19 -5
  115. package/src/modules/catalog/components/products/ProductUomSection.tsx +745 -0
  116. package/src/modules/catalog/components/products/productForm.ts +369 -256
  117. package/src/modules/catalog/components/products/productFormUtils.ts +82 -0
  118. package/src/modules/catalog/data/entities.ts +82 -1
  119. package/src/modules/catalog/data/validators.ts +118 -34
  120. package/src/modules/catalog/events.ts +3 -0
  121. package/src/modules/catalog/i18n/de.json +56 -0
  122. package/src/modules/catalog/i18n/en.json +56 -0
  123. package/src/modules/catalog/i18n/es.json +56 -0
  124. package/src/modules/catalog/i18n/pl.json +56 -0
  125. package/src/modules/catalog/lib/unitCodes.ts +1 -0
  126. package/src/modules/catalog/lib/unitResolution.ts +62 -0
  127. package/src/modules/catalog/migrations/.snapshot-open-mercato.json +245 -0
  128. package/src/modules/catalog/migrations/Migration20260218225422.ts +21 -0
  129. package/src/modules/catalog/migrations/Migration20260219084500.ts +26 -0
  130. package/src/modules/catalog/search.ts +73 -1
  131. package/src/modules/catalog/seed/examples.ts +552 -479
  132. package/src/modules/dashboards/i18n/de.json +1 -1
  133. package/src/modules/dashboards/i18n/en.json +1 -1
  134. package/src/modules/dashboards/i18n/es.json +1 -1
  135. package/src/modules/dashboards/i18n/pl.json +1 -1
  136. package/src/modules/dashboards/seed/analytics.ts +3 -0
  137. package/src/modules/sales/api/order-lines/route.ts +158 -68
  138. package/src/modules/sales/api/quote-lines/route.ts +161 -67
  139. package/src/modules/sales/api/quotes/public/[token]/route.ts +122 -36
  140. package/src/modules/sales/commands/documents.ts +4250 -2424
  141. package/src/modules/sales/commands/shared.ts +7 -2
  142. package/src/modules/sales/components/documents/ItemsSection.tsx +580 -310
  143. package/src/modules/sales/components/documents/LineItemDialog.tsx +1988 -833
  144. package/src/modules/sales/components/documents/ShipmentsSection.tsx +17 -3
  145. package/src/modules/sales/components/documents/lineItemTypes.ts +6 -0
  146. package/src/modules/sales/data/entities.ts +53 -0
  147. package/src/modules/sales/data/validators.ts +36 -0
  148. package/src/modules/sales/frontend/quote/[token]/page.tsx +25 -1
  149. package/src/modules/sales/i18n/de.json +23 -3
  150. package/src/modules/sales/i18n/en.json +23 -3
  151. package/src/modules/sales/i18n/es.json +23 -3
  152. package/src/modules/sales/i18n/pl.json +23 -3
  153. package/src/modules/sales/lib/types.ts +30 -0
  154. package/src/modules/sales/migrations/.snapshot-open-mercato.json +172 -0
  155. package/src/modules/sales/migrations/Migration20260218225423.ts +37 -0
  156. package/src/modules/sales/migrations/Migration20260219084501.ts +73 -0
  157. package/src/modules/sales/search.ts +28 -0
  158. package/src/modules/sales/seed/examples.ts +20 -1
  159. package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +1 -1
  160. package/src/modules/workflows/i18n/de.json +4 -4
  161. package/src/modules/workflows/i18n/en.json +4 -4
  162. package/src/modules/workflows/i18n/es.json +4 -4
  163. package/src/modules/workflows/i18n/pl.json +4 -4
@@ -1,9 +1,9 @@
1
- import { randomUUID } from 'node:crypto'
2
- import path from 'node:path'
3
- import { promises as fs } from 'node:fs'
4
- import type { EntityManager } from '@mikro-orm/postgresql'
5
- import type { AwilixContainer } from 'awilix'
6
- import { SalesChannel } from '@open-mercato/core/modules/sales/data/entities'
1
+ import { randomUUID } from "node:crypto";
2
+ import path from "node:path";
3
+ import { promises as fs } from "node:fs";
4
+ import type { EntityManager } from "@mikro-orm/postgresql";
5
+ import type { AwilixContainer } from "awilix";
6
+ import { SalesChannel } from "@open-mercato/core/modules/sales/data/entities";
7
7
  import {
8
8
  CatalogOffer,
9
9
  CatalogPriceKind,
@@ -12,45 +12,59 @@ import {
12
12
  CatalogProductCategoryAssignment,
13
13
  CatalogProductPrice,
14
14
  CatalogProductVariant,
15
- } from '../data/entities'
16
- import { DefaultDataEngine } from '@open-mercato/shared/lib/data/engine'
17
- import { ensureCustomFieldDefinitions, type FieldSetInput } from '@open-mercato/core/modules/entities/lib/field-definitions'
18
- import { CustomFieldEntityConfig } from '@open-mercato/core/modules/entities/data/entities'
19
- import { rebuildCategoryHierarchyForOrganization } from '../lib/categoryHierarchy'
20
- import { defineFields, cf } from '@open-mercato/shared/modules/dsl'
21
- import { E } from '#generated/entities.ids.generated'
22
- import { SalesTaxRate } from '@open-mercato/core/modules/sales/data/entities'
23
- import { Attachment, AttachmentPartition } from '@open-mercato/core/modules/attachments/data/entities'
24
- import { ensureDefaultPartitions, resolveDefaultPartitionCode } from '@open-mercato/core/modules/attachments/lib/partitions'
25
- import { storePartitionFile } from '@open-mercato/core/modules/attachments/lib/storage'
26
- import { mergeAttachmentMetadata } from '@open-mercato/core/modules/attachments/lib/metadata'
27
- import { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'
28
-
29
- type SeedScope = { tenantId: string; organizationId: string }
30
-
31
- const EXAMPLES_MEDIA_ROOT = path.join(process.cwd(), 'public', 'examples')
15
+ } from "../data/entities";
16
+ import { DefaultDataEngine } from "@open-mercato/shared/lib/data/engine";
17
+ import {
18
+ ensureCustomFieldDefinitions,
19
+ type FieldSetInput,
20
+ } from "@open-mercato/core/modules/entities/lib/field-definitions";
21
+ import { CustomFieldEntityConfig } from "@open-mercato/core/modules/entities/data/entities";
22
+ import { rebuildCategoryHierarchyForOrganization } from "../lib/categoryHierarchy";
23
+ import { defineFields, cf } from "@open-mercato/shared/modules/dsl";
24
+ import { E } from "#generated/entities.ids.generated";
25
+ import { SalesTaxRate } from "@open-mercato/core/modules/sales/data/entities";
26
+ import {
27
+ Attachment,
28
+ AttachmentPartition,
29
+ } from "@open-mercato/core/modules/attachments/data/entities";
30
+ import {
31
+ ensureDefaultPartitions,
32
+ resolveDefaultPartitionCode,
33
+ } from "@open-mercato/core/modules/attachments/lib/partitions";
34
+ import { storePartitionFile } from "@open-mercato/core/modules/attachments/lib/storage";
35
+ import { mergeAttachmentMetadata } from "@open-mercato/core/modules/attachments/lib/metadata";
36
+ import {
37
+ buildAttachmentFileUrl,
38
+ buildAttachmentImageUrl,
39
+ slugifyAttachmentFileName,
40
+ } from "@open-mercato/core/modules/attachments/lib/imageUrls";
41
+ import { canonicalizeUnitCode } from "../lib/unitCodes";
42
+
43
+ type SeedScope = { tenantId: string; organizationId: string };
44
+
45
+ const EXAMPLES_MEDIA_ROOT = path.join(process.cwd(), "public", "examples");
32
46
 
33
47
  function detectMimeType(fileName: string): string {
34
- const ext = fileName.toLowerCase().split('.').pop() || ''
35
- if (ext === 'png') return 'image/png'
36
- if (ext === 'jpg' || ext === 'jpeg') return 'image/jpeg'
37
- if (ext === 'webp') return 'image/webp'
38
- return 'application/octet-stream'
48
+ const ext = fileName.toLowerCase().split(".").pop() || "";
49
+ if (ext === "png") return "image/png";
50
+ if (ext === "jpg" || ext === "jpeg") return "image/jpeg";
51
+ if (ext === "webp") return "image/webp";
52
+ return "application/octet-stream";
39
53
  }
40
54
 
41
55
  async function ensureAttachmentPartition(
42
56
  em: EntityManager,
43
- code: string
57
+ code: string,
44
58
  ): Promise<AttachmentPartition> {
45
- let partition = await em.findOne(AttachmentPartition, { code })
59
+ let partition = await em.findOne(AttachmentPartition, { code });
46
60
  if (!partition) {
47
- await ensureDefaultPartitions(em)
48
- partition = await em.findOne(AttachmentPartition, { code })
61
+ await ensureDefaultPartitions(em);
62
+ partition = await em.findOne(AttachmentPartition, { code });
49
63
  }
50
64
  if (!partition) {
51
- throw new Error(`Attachment partition "${code}" is not configured.`)
65
+ throw new Error(`Attachment partition "${code}" is not configured.`);
52
66
  }
53
- return partition
67
+ return partition;
54
68
  }
55
69
 
56
70
  async function attachMediaFromExamples(
@@ -58,20 +72,20 @@ async function attachMediaFromExamples(
58
72
  scope: SeedScope,
59
73
  entityId: string,
60
74
  recordId: string,
61
- mediaSeeds?: MediaSeed[]
75
+ mediaSeeds?: MediaSeed[],
62
76
  ): Promise<Array<{ id: string; imageUrl: string }>> {
63
- if (!mediaSeeds?.length) return []
64
- const partitionCode = resolveDefaultPartitionCode(entityId)
65
- const partition = await ensureAttachmentPartition(em, partitionCode)
66
- const results: Array<{ id: string; imageUrl: string }> = []
77
+ if (!mediaSeeds?.length) return [];
78
+ const partitionCode = resolveDefaultPartitionCode(entityId);
79
+ const partition = await ensureAttachmentPartition(em, partitionCode);
80
+ const results: Array<{ id: string; imageUrl: string }> = [];
67
81
  for (const media of mediaSeeds) {
68
- const sourcePath = path.join(EXAMPLES_MEDIA_ROOT, media.file)
69
- let buffer: Buffer
82
+ const sourcePath = path.join(EXAMPLES_MEDIA_ROOT, media.file);
83
+ let buffer: Buffer;
70
84
  try {
71
- buffer = await fs.readFile(sourcePath)
85
+ buffer = await fs.readFile(sourcePath);
72
86
  } catch (error) {
73
- console.warn(`[catalog.seed] Example media missing: ${sourcePath}`)
74
- continue
87
+ console.warn(`[catalog.seed] Example media missing: ${sourcePath}`);
88
+ continue;
75
89
  }
76
90
  const stored = await storePartitionFile({
77
91
  partitionCode: partition.code,
@@ -79,12 +93,12 @@ async function attachMediaFromExamples(
79
93
  tenantId: scope.tenantId,
80
94
  fileName: media.file,
81
95
  buffer,
82
- })
83
- const attachmentId = randomUUID()
84
- const slug = slugifyAttachmentFileName(media.file, 'media')
96
+ });
97
+ const attachmentId = randomUUID();
98
+ const slug = slugifyAttachmentFileName(media.file, "media");
85
99
  const metadata = mergeAttachmentMetadata(null, {
86
100
  assignments: [{ type: entityId, id: recordId }],
87
- })
101
+ });
88
102
  const attachment = em.create(Attachment, {
89
103
  id: attachmentId,
90
104
  entityId,
@@ -95,502 +109,527 @@ async function attachMediaFromExamples(
95
109
  fileName: media.file,
96
110
  mimeType: detectMimeType(media.file),
97
111
  fileSize: buffer.length,
98
- storageDriver: partition.storageDriver || 'local',
112
+ storageDriver: partition.storageDriver || "local",
99
113
  storagePath: stored.storagePath,
100
114
  storageMetadata: metadata,
101
115
  url: buildAttachmentFileUrl(attachmentId),
102
- })
103
- em.persist(attachment)
116
+ });
117
+ em.persist(attachment);
104
118
  results.push({
105
119
  id: attachmentId,
106
120
  imageUrl: buildAttachmentImageUrl(attachmentId, { slug }),
107
- })
121
+ });
108
122
  }
109
- return results
123
+ return results;
110
124
  }
111
125
 
112
126
  const PRODUCT_FIELDSETS = [
113
127
  {
114
- code: 'fashion_mens_footwear',
115
- label: 'Fashion · Men · Footwear',
116
- icon: 'carbon:sneaker',
117
- description: 'Material, construction, and care metadata for men’s performance footwear.',
128
+ code: "fashion_mens_footwear",
129
+ label: "Fashion · Men · Footwear",
130
+ icon: "carbon:sneaker",
131
+ description:
132
+ "Material, construction, and care metadata for men’s performance footwear.",
118
133
  groups: [
119
- { code: 'identity', title: 'Identity' },
120
- { code: 'materials', title: 'Materials & Build' },
121
- { code: 'care', title: 'Care instructions' },
134
+ { code: "identity", title: "Identity" },
135
+ { code: "materials", title: "Materials & Build" },
136
+ { code: "care", title: "Care instructions" },
122
137
  ],
123
138
  },
124
139
  {
125
- code: 'fashion_womens_dresses',
126
- label: 'Fashion · Women · Dresses & Jumpsuits',
127
- icon: 'solar:dress-linear',
128
- description: 'Silhouette, fabric, and care metadata for womenswear.',
140
+ code: "fashion_womens_dresses",
141
+ label: "Fashion · Women · Dresses & Jumpsuits",
142
+ icon: "solar:dress-linear",
143
+ description: "Silhouette, fabric, and care metadata for womenswear.",
129
144
  groups: [
130
- { code: 'identity', title: 'Identity' },
131
- { code: 'materials', title: 'Materials' },
132
- { code: 'fit', title: 'Fit & Length' },
133
- { code: 'care', title: 'Care instructions' },
145
+ { code: "identity", title: "Identity" },
146
+ { code: "materials", title: "Materials" },
147
+ { code: "fit", title: "Fit & Length" },
148
+ { code: "care", title: "Care instructions" },
134
149
  ],
135
150
  },
136
151
  {
137
- code: 'service_schedule',
138
- label: 'Services · Scheduling',
139
- icon: 'solar:calendar-linear',
140
- description: 'Scheduling, preparation, and delivery metadata for service offerings.',
152
+ code: "service_schedule",
153
+ label: "Services · Scheduling",
154
+ icon: "solar:calendar-linear",
155
+ description:
156
+ "Scheduling, preparation, and delivery metadata for service offerings.",
141
157
  groups: [
142
- { code: 'identity', title: 'Identity' },
143
- { code: 'timing', title: 'Timing rules' },
144
- { code: 'resources', title: 'Resources & Delivery' },
158
+ { code: "identity", title: "Identity" },
159
+ { code: "timing", title: "Timing rules" },
160
+ { code: "resources", title: "Resources & Delivery" },
145
161
  ],
146
162
  },
147
- ] as const
163
+ ] as const;
148
164
 
149
165
  const VARIANT_FIELDSETS = [
150
166
  {
151
- code: 'fashion_mens_footwear',
152
- label: 'Fashion · Men · Footwear',
153
- icon: 'carbon:sneaker',
154
- description: 'Variant-level sizing metadata for men’s footwear.',
167
+ code: "fashion_mens_footwear",
168
+ label: "Fashion · Men · Footwear",
169
+ icon: "carbon:sneaker",
170
+ description: "Variant-level sizing metadata for men’s footwear.",
155
171
  groups: [
156
- { code: 'fit', title: 'Fit' },
157
- { code: 'finish', title: 'Finish' },
172
+ { code: "fit", title: "Fit" },
173
+ { code: "finish", title: "Finish" },
158
174
  ],
159
175
  },
160
176
  {
161
- code: 'fashion_womens_dresses',
162
- label: 'Fashion · Women · Dresses & Jumpsuits',
163
- icon: 'solar:dress-linear',
164
- description: 'Variant-level sizing metadata for womenswear.',
177
+ code: "fashion_womens_dresses",
178
+ label: "Fashion · Women · Dresses & Jumpsuits",
179
+ icon: "solar:dress-linear",
180
+ description: "Variant-level sizing metadata for womenswear.",
165
181
  groups: [
166
- { code: 'fit', title: 'Fit' },
167
- { code: 'finish', title: 'Finish' },
182
+ { code: "fit", title: "Fit" },
183
+ { code: "finish", title: "Finish" },
168
184
  ],
169
185
  },
170
186
  {
171
- code: 'service_schedule',
172
- label: 'Services · Scheduling',
173
- icon: 'solar:calendar-linear',
174
- description: 'Provider, duration, and environment metadata for service slots.',
187
+ code: "service_schedule",
188
+ label: "Services · Scheduling",
189
+ icon: "solar:calendar-linear",
190
+ description:
191
+ "Provider, duration, and environment metadata for service slots.",
175
192
  groups: [
176
- { code: 'provider', title: 'Provider' },
177
- { code: 'environment', title: 'Environment' },
193
+ { code: "provider", title: "Provider" },
194
+ { code: "environment", title: "Environment" },
178
195
  ],
179
196
  },
180
- ] as const
197
+ ] as const;
181
198
 
182
199
  const CUSTOM_FIELD_SETS: FieldSetInput[] = [
183
200
  defineFields(E.catalog.catalog_product, [
184
- cf.text('style_code', {
185
- label: 'Style code',
186
- description: 'Reference code shared with merchandising teams.',
187
- filterable: true,
188
- fieldset: 'fashion_mens_footwear',
189
- group: { code: 'identity' },
190
- }),
191
- cf.select('upper_material', ['engineered_knit', 'full_grain_leather', 'recycled_mesh'], {
192
- label: 'Upper material',
193
- fieldset: 'fashion_mens_footwear',
194
- group: { code: 'materials' },
201
+ cf.text("style_code", {
202
+ label: "Style code",
203
+ description: "Reference code shared with merchandising teams.",
195
204
  filterable: true,
205
+ fieldset: "fashion_mens_footwear",
206
+ group: { code: "identity" },
196
207
  }),
197
- cf.select('cushioning_profile', ['responsive', 'plush', 'stability'], {
198
- label: 'Cushioning profile',
199
- fieldset: 'fashion_mens_footwear',
200
- group: { code: 'materials' },
208
+ cf.select(
209
+ "upper_material",
210
+ ["engineered_knit", "full_grain_leather", "recycled_mesh"],
211
+ {
212
+ label: "Upper material",
213
+ fieldset: "fashion_mens_footwear",
214
+ group: { code: "materials" },
215
+ filterable: true,
216
+ },
217
+ ),
218
+ cf.select("cushioning_profile", ["responsive", "plush", "stability"], {
219
+ label: "Cushioning profile",
220
+ fieldset: "fashion_mens_footwear",
221
+ group: { code: "materials" },
201
222
  }),
202
- cf.multiline('care_notes', {
203
- label: 'Care notes',
204
- editor: 'markdown',
205
- fieldset: 'fashion_mens_footwear',
206
- group: { code: 'care' },
223
+ cf.multiline("care_notes", {
224
+ label: "Care notes",
225
+ editor: "markdown",
226
+ fieldset: "fashion_mens_footwear",
227
+ group: { code: "care" },
207
228
  }),
208
229
  ]),
209
230
  defineFields(E.catalog.catalog_product, [
210
- cf.select('silhouette', ['wrap', 'column', 'fit_and_flare', 'jumpsuit'], {
211
- label: 'Silhouette',
212
- fieldset: 'fashion_womens_dresses',
213
- group: { code: 'identity' },
231
+ cf.select("silhouette", ["wrap", "column", "fit_and_flare", "jumpsuit"], {
232
+ label: "Silhouette",
233
+ fieldset: "fashion_womens_dresses",
234
+ group: { code: "identity" },
214
235
  filterable: true,
215
236
  }),
216
- cf.select('fabric_mix', ['silk_blend', 'recycled_poly', 'linen', 'cupro'], {
217
- label: 'Fabric mix',
218
- fieldset: 'fashion_womens_dresses',
219
- group: { code: 'materials' },
237
+ cf.select("fabric_mix", ["silk_blend", "recycled_poly", "linen", "cupro"], {
238
+ label: "Fabric mix",
239
+ fieldset: "fashion_womens_dresses",
240
+ group: { code: "materials" },
220
241
  }),
221
- cf.select('occasion_ready', ['daytime', 'evening', 'resort'], {
222
- label: 'Occasion',
223
- fieldset: 'fashion_womens_dresses',
224
- group: { code: 'fit' },
242
+ cf.select("occasion_ready", ["daytime", "evening", "resort"], {
243
+ label: "Occasion",
244
+ fieldset: "fashion_womens_dresses",
245
+ group: { code: "fit" },
225
246
  }),
226
- cf.multiline('finishing_details', {
227
- label: 'Finishing details',
228
- editor: 'markdown',
229
- fieldset: 'fashion_womens_dresses',
230
- group: { code: 'care' },
247
+ cf.multiline("finishing_details", {
248
+ label: "Finishing details",
249
+ editor: "markdown",
250
+ fieldset: "fashion_womens_dresses",
251
+ group: { code: "care" },
231
252
  }),
232
253
  ]),
233
254
  defineFields(E.catalog.catalog_product_variant, [
234
- cf.integer('shoe_size', {
235
- label: 'US size',
236
- fieldset: 'fashion_mens_footwear',
237
- group: { code: 'fit' },
255
+ cf.integer("shoe_size", {
256
+ label: "US size",
257
+ fieldset: "fashion_mens_footwear",
258
+ group: { code: "fit" },
238
259
  filterable: true,
239
260
  }),
240
- cf.select('shoe_width', ['B', 'D', 'EE'], {
241
- label: 'Width',
242
- fieldset: 'fashion_mens_footwear',
243
- group: { code: 'fit' },
261
+ cf.select("shoe_width", ["B", "D", "EE"], {
262
+ label: "Width",
263
+ fieldset: "fashion_mens_footwear",
264
+ group: { code: "fit" },
244
265
  }),
245
- cf.text('colorway', {
246
- label: 'Colorway',
247
- fieldset: 'fashion_mens_footwear',
248
- group: { code: 'finish' },
266
+ cf.text("colorway", {
267
+ label: "Colorway",
268
+ fieldset: "fashion_mens_footwear",
269
+ group: { code: "finish" },
249
270
  }),
250
271
  ]),
251
272
  defineFields(E.catalog.catalog_product_variant, [
252
- cf.integer('numeric_size', {
253
- label: 'Numeric size',
254
- fieldset: 'fashion_womens_dresses',
255
- group: { code: 'fit' },
273
+ cf.integer("numeric_size", {
274
+ label: "Numeric size",
275
+ fieldset: "fashion_womens_dresses",
276
+ group: { code: "fit" },
256
277
  }),
257
- cf.select('length_profile', ['mini', 'midi', 'maxi'], {
258
- label: 'Length',
259
- fieldset: 'fashion_womens_dresses',
260
- group: { code: 'fit' },
278
+ cf.select("length_profile", ["mini", "midi", "maxi"], {
279
+ label: "Length",
280
+ fieldset: "fashion_womens_dresses",
281
+ group: { code: "fit" },
261
282
  }),
262
- cf.text('color_story', {
263
- label: 'Color story',
264
- fieldset: 'fashion_womens_dresses',
265
- group: { code: 'finish' },
283
+ cf.text("color_story", {
284
+ label: "Color story",
285
+ fieldset: "fashion_womens_dresses",
286
+ group: { code: "finish" },
266
287
  }),
267
288
  ]),
268
289
  defineFields(E.catalog.catalog_product, [
269
- cf.integer('service_duration_minutes', {
270
- label: 'Duration (minutes)',
271
- description: 'Length of a single service slot.',
272
- fieldset: 'service_schedule',
273
- group: { code: 'timing' },
290
+ cf.integer("service_duration_minutes", {
291
+ label: "Duration (minutes)",
292
+ description: "Length of a single service slot.",
293
+ fieldset: "service_schedule",
294
+ group: { code: "timing" },
274
295
  filterable: true,
275
296
  required: true,
276
297
  }),
277
- cf.integer('service_buffer_minutes', {
278
- label: 'Buffer between appointments',
279
- description: 'Minimum downtime between consecutive service slots (minutes).',
280
- fieldset: 'service_schedule',
281
- group: { code: 'timing' },
298
+ cf.integer("service_buffer_minutes", {
299
+ label: "Buffer between appointments",
300
+ description:
301
+ "Minimum downtime between consecutive service slots (minutes).",
302
+ fieldset: "service_schedule",
303
+ group: { code: "timing" },
282
304
  }),
283
- cf.text('service_location', {
284
- label: 'Location / Room',
285
- description: 'Where the service is delivered.',
286
- fieldset: 'service_schedule',
287
- group: { code: 'identity' },
305
+ cf.text("service_location", {
306
+ label: "Location / Room",
307
+ description: "Where the service is delivered.",
308
+ fieldset: "service_schedule",
309
+ group: { code: "identity" },
288
310
  }),
289
- cf.select('service_resources', ['stylist', 'therapist', 'treatment_room', 'wash_station', 'steam_room'], {
290
- label: 'Required resources',
291
- description: 'Staff or rooms required to perform the service.',
292
- fieldset: 'service_schedule',
293
- group: { code: 'resources' },
294
- multi: true,
295
- }),
296
- cf.boolean('service_remote_available', {
297
- label: 'Remote session available',
298
- description: 'Indicates whether the service can be performed remotely or virtually.',
299
- fieldset: 'service_schedule',
300
- group: { code: 'resources' },
311
+ cf.select(
312
+ "service_resources",
313
+ ["stylist", "therapist", "treatment_room", "wash_station", "steam_room"],
314
+ {
315
+ label: "Required resources",
316
+ description: "Staff or rooms required to perform the service.",
317
+ fieldset: "service_schedule",
318
+ group: { code: "resources" },
319
+ multi: true,
320
+ },
321
+ ),
322
+ cf.boolean("service_remote_available", {
323
+ label: "Remote session available",
324
+ description:
325
+ "Indicates whether the service can be performed remotely or virtually.",
326
+ fieldset: "service_schedule",
327
+ group: { code: "resources" },
301
328
  defaultValue: false,
302
329
  }),
303
330
  ]),
304
331
  defineFields(E.catalog.catalog_product_variant, [
305
- cf.select('provider_level', ['junior', 'senior', 'master'], {
306
- label: 'Provider level',
307
- description: 'Seniority of the assigned specialist.',
308
- fieldset: 'service_schedule',
309
- group: { code: 'provider' },
332
+ cf.select("provider_level", ["junior", "senior", "master"], {
333
+ label: "Provider level",
334
+ description: "Seniority of the assigned specialist.",
335
+ fieldset: "service_schedule",
336
+ group: { code: "provider" },
310
337
  filterable: true,
311
338
  }),
312
- cf.text('staff_member', {
313
- label: 'Staff member',
314
- description: 'Optional name of the staff member who usually delivers this variant.',
315
- fieldset: 'service_schedule',
316
- group: { code: 'provider' },
339
+ cf.text("staff_member", {
340
+ label: "Staff member",
341
+ description:
342
+ "Optional name of the staff member who usually delivers this variant.",
343
+ fieldset: "service_schedule",
344
+ group: { code: "provider" },
317
345
  }),
318
- cf.select('environment_type', ['studio', 'suite', 'on_site'], {
319
- label: 'Environment',
320
- description: 'Where the session is hosted.',
321
- fieldset: 'service_schedule',
322
- group: { code: 'environment' },
346
+ cf.select("environment_type", ["studio", "suite", "on_site"], {
347
+ label: "Environment",
348
+ description: "Where the session is hosted.",
349
+ fieldset: "service_schedule",
350
+ group: { code: "environment" },
323
351
  }),
324
352
  ]),
325
- ]
353
+ ];
326
354
 
327
355
  type CategorySeed = {
328
- slug: string
329
- name: string
330
- description?: string
331
- children?: CategorySeed[]
332
- }
356
+ slug: string;
357
+ name: string;
358
+ description?: string;
359
+ children?: CategorySeed[];
360
+ };
333
361
 
334
362
  const CATEGORY_TREE: CategorySeed[] = [
335
363
  {
336
- slug: 'fashion',
337
- name: 'Fashion',
338
- description: 'Seasonal assortments and vertical-specific collections.',
364
+ slug: "fashion",
365
+ name: "Fashion",
366
+ description: "Seasonal assortments and vertical-specific collections.",
339
367
  children: [
340
368
  {
341
- slug: 'fashion-men',
342
- name: 'Men',
369
+ slug: "fashion-men",
370
+ name: "Men",
343
371
  children: [
344
372
  {
345
- slug: 'fashion-men-footwear',
346
- name: 'Footwear',
347
- description: 'Premium sneakers, boots, and sandals.',
373
+ slug: "fashion-men-footwear",
374
+ name: "Footwear",
375
+ description: "Premium sneakers, boots, and sandals.",
348
376
  },
349
377
  ],
350
378
  },
351
379
  {
352
- slug: 'fashion-women',
353
- name: 'Women',
380
+ slug: "fashion-women",
381
+ name: "Women",
354
382
  children: [
355
383
  {
356
- slug: 'fashion-women-dresses-jumpsuits',
357
- name: 'Dresses & Jumpsuits',
358
- description: 'Occasion-ready dresses and tailored jumpsuits.',
384
+ slug: "fashion-women-dresses-jumpsuits",
385
+ name: "Dresses & Jumpsuits",
386
+ description: "Occasion-ready dresses and tailored jumpsuits.",
359
387
  },
360
388
  ],
361
389
  },
362
390
  ],
363
391
  },
364
392
  {
365
- slug: 'services',
366
- name: 'Services',
367
- description: 'Bookable in-person and virtual experiences.',
393
+ slug: "services",
394
+ name: "Services",
395
+ description: "Bookable in-person and virtual experiences.",
368
396
  children: [
369
397
  {
370
- slug: 'services-hairdresser',
371
- name: 'Hairdresser',
372
- description: 'Salon services ranging from quick trims to signature looks.',
398
+ slug: "services-hairdresser",
399
+ name: "Hairdresser",
400
+ description:
401
+ "Salon services ranging from quick trims to signature looks.",
373
402
  },
374
403
  {
375
- slug: 'services-massage',
376
- name: 'Massage',
377
- description: 'Wellness treatments and bodywork sessions.',
404
+ slug: "services-massage",
405
+ name: "Massage",
406
+ description: "Wellness treatments and bodywork sessions.",
378
407
  },
379
408
  ],
380
409
  },
381
- ]
410
+ ];
382
411
 
383
412
  type MediaSeed = {
384
- file: string
385
- title?: string
386
- }
413
+ file: string;
414
+ title?: string;
415
+ };
387
416
 
388
417
  type VariantSeed = {
389
- name: string
390
- sku: string
391
- isDefault?: boolean
392
- optionValues?: Record<string, string>
418
+ name: string;
419
+ sku: string;
420
+ isDefault?: boolean;
421
+ optionValues?: Record<string, string>;
393
422
  prices: {
394
- regular: number
395
- sale?: number
396
- }
397
- customFields?: Record<string, string | number | boolean | null>
398
- media?: MediaSeed[]
399
- }
423
+ regular: number;
424
+ sale?: number;
425
+ };
426
+ customFields?: Record<string, string | number | boolean | null>;
427
+ media?: MediaSeed[];
428
+ };
400
429
 
401
430
  type ProductSeed = {
402
- title: string
403
- handle: string
404
- sku?: string
405
- description: string
406
- categorySlug: string
407
- customFieldsetCode: string
408
- variantFieldsetCode: string
409
- unit: string
410
- metadata?: Record<string, unknown>
411
- customFields?: Record<string, string | number | boolean | null>
412
- media?: MediaSeed[]
413
- variants: VariantSeed[]
414
- }
431
+ title: string;
432
+ handle: string;
433
+ sku?: string;
434
+ description: string;
435
+ categorySlug: string;
436
+ customFieldsetCode: string;
437
+ variantFieldsetCode: string;
438
+ unit: string;
439
+ metadata?: Record<string, unknown>;
440
+ customFields?: Record<string, string | number | boolean | null>;
441
+ media?: MediaSeed[];
442
+ variants: VariantSeed[];
443
+ };
415
444
 
416
445
  const PRODUCT_SEEDS: ProductSeed[] = [
417
446
  {
418
- title: 'Atlas Runner Sneaker',
419
- handle: 'atlas-runner-sneaker',
420
- sku: 'ATLAS-RUNNER',
447
+ title: "Atlas Runner Sneaker",
448
+ handle: "atlas-runner-sneaker",
449
+ sku: "ATLAS-RUNNER",
421
450
  description:
422
- 'Lightweight road sneaker engineered with a breathable knit upper, recycled TPU overlays, and a decoupled heel for smooth transitions.',
423
- categorySlug: 'fashion-men-footwear',
424
- customFieldsetCode: 'fashion_mens_footwear',
425
- variantFieldsetCode: 'fashion_mens_footwear',
426
- unit: 'pair',
427
- metadata: { division: 'RunLab', season: 'SS25' },
451
+ "Lightweight road sneaker engineered with a breathable knit upper, recycled TPU overlays, and a decoupled heel for smooth transitions.",
452
+ categorySlug: "fashion-men-footwear",
453
+ customFieldsetCode: "fashion_mens_footwear",
454
+ variantFieldsetCode: "fashion_mens_footwear",
455
+ unit: "pair",
456
+ metadata: { division: "RunLab", season: "SS25" },
428
457
  customFields: {
429
- style_code: 'AR-2025',
430
- upper_material: 'engineered_knit',
431
- cushioning_profile: 'responsive',
432
- care_notes: 'Spot clean after each run and air dry. Avoid machine drying.',
458
+ style_code: "AR-2025",
459
+ upper_material: "engineered_knit",
460
+ cushioning_profile: "responsive",
461
+ care_notes:
462
+ "Spot clean after each run and air dry. Avoid machine drying.",
433
463
  },
434
- media: [
435
- { file: 'atlas-runner-midnight-1.png' },
436
- ],
464
+ media: [{ file: "atlas-runner-midnight-1.png" }],
437
465
  variants: [
438
466
  {
439
- name: 'Midnight Navy · US 8',
440
- sku: 'ATLAS-RUN-NAVY-8',
467
+ name: "Midnight Navy · US 8",
468
+ sku: "ATLAS-RUN-NAVY-8",
441
469
  isDefault: true,
442
- optionValues: { color: 'Midnight Navy', size: 'US 8' },
470
+ optionValues: { color: "Midnight Navy", size: "US 8" },
443
471
  prices: { regular: 168, sale: 148 },
444
- customFields: { shoe_size: 8, shoe_width: 'D', colorway: 'Midnight Navy' },
472
+ customFields: {
473
+ shoe_size: 8,
474
+ shoe_width: "D",
475
+ colorway: "Midnight Navy",
476
+ },
445
477
  media: [
446
- { file: 'atlas-runner-midnight-1.png' },
447
- { file: 'atlas-runner-midnight-2.png' },
478
+ { file: "atlas-runner-midnight-1.png" },
479
+ { file: "atlas-runner-midnight-2.png" },
448
480
  ],
449
481
  },
450
482
  {
451
- name: 'Glacier Grey · US 10',
452
- sku: 'ATLAS-RUN-GLACIER-10',
453
- optionValues: { color: 'Glacier Grey', size: 'US 10' },
483
+ name: "Glacier Grey · US 10",
484
+ sku: "ATLAS-RUN-GLACIER-10",
485
+ optionValues: { color: "Glacier Grey", size: "US 10" },
454
486
  prices: { regular: 168, sale: 138 },
455
- customFields: { shoe_size: 10, shoe_width: 'EE', colorway: 'Glacier Grey' },
487
+ customFields: {
488
+ shoe_size: 10,
489
+ shoe_width: "EE",
490
+ colorway: "Glacier Grey",
491
+ },
456
492
  media: [
457
- { file: 'atlas-runner-glacier-1.png' },
458
- { file: 'atlas-runner-glacier-2.png' },
493
+ { file: "atlas-runner-glacier-1.png" },
494
+ { file: "atlas-runner-glacier-2.png" },
459
495
  ],
460
496
  },
461
497
  ],
462
498
  },
463
499
  {
464
- title: 'Aurora Wrap Dress',
465
- handle: 'aurora-wrap-dress',
466
- sku: 'AURORA-WRAP',
500
+ title: "Aurora Wrap Dress",
501
+ handle: "aurora-wrap-dress",
502
+ sku: "AURORA-WRAP",
467
503
  description:
468
- 'Bias-cut wrap dress with blouson sleeves, matte silk blend, and hidden interior snaps so the placket stays put at events.',
469
- categorySlug: 'fashion-women-dresses-jumpsuits',
470
- customFieldsetCode: 'fashion_womens_dresses',
471
- variantFieldsetCode: 'fashion_womens_dresses',
472
- unit: 'unit',
473
- metadata: { capsule: 'Evening Atelier', season: 'Resort 25' },
504
+ "Bias-cut wrap dress with blouson sleeves, matte silk blend, and hidden interior snaps so the placket stays put at events.",
505
+ categorySlug: "fashion-women-dresses-jumpsuits",
506
+ customFieldsetCode: "fashion_womens_dresses",
507
+ variantFieldsetCode: "fashion_womens_dresses",
508
+ unit: "unit",
509
+ metadata: { capsule: "Evening Atelier", season: "Resort 25" },
474
510
  customFields: {
475
- silhouette: 'wrap',
476
- fabric_mix: 'silk_blend',
477
- occasion_ready: 'evening',
478
- finishing_details: 'Hand-finished hem with subtle tonal beading along the wrap edge.',
511
+ silhouette: "wrap",
512
+ fabric_mix: "silk_blend",
513
+ occasion_ready: "evening",
514
+ finishing_details:
515
+ "Hand-finished hem with subtle tonal beading along the wrap edge.",
479
516
  },
480
- media: [
481
- { file: 'aurora-wrap-rosewood.png' },
482
- ],
517
+ media: [{ file: "aurora-wrap-rosewood.png" }],
483
518
  variants: [
484
519
  {
485
- name: 'Rosewood · Medium',
486
- sku: 'AURORA-ROSE-M',
520
+ name: "Rosewood · Medium",
521
+ sku: "AURORA-ROSE-M",
487
522
  isDefault: true,
488
- optionValues: { color: 'Rosewood', size: 'Medium' },
523
+ optionValues: { color: "Rosewood", size: "Medium" },
489
524
  prices: { regular: 248, sale: 212 },
490
- customFields: { numeric_size: 6, length_profile: 'midi', color_story: 'Rosewood' },
491
- media: [
492
- { file: 'aurora-wrap-rosewood.png' },
493
- ],
525
+ customFields: {
526
+ numeric_size: 6,
527
+ length_profile: "midi",
528
+ color_story: "Rosewood",
529
+ },
530
+ media: [{ file: "aurora-wrap-rosewood.png" }],
494
531
  },
495
532
  {
496
- name: 'Celestial · Large',
497
- sku: 'AURORA-CELESTIAL-L',
498
- optionValues: { color: 'Celestial', size: 'Large' },
533
+ name: "Celestial · Large",
534
+ sku: "AURORA-CELESTIAL-L",
535
+ optionValues: { color: "Celestial", size: "Large" },
499
536
  prices: { regular: 248, sale: 198 },
500
- customFields: { numeric_size: 8, length_profile: 'maxi', color_story: 'Celestial blue' },
501
- media: [
502
- { file: 'aurora-wrap-celestial.png' },
503
- ],
537
+ customFields: {
538
+ numeric_size: 8,
539
+ length_profile: "maxi",
540
+ color_story: "Celestial blue",
541
+ },
542
+ media: [{ file: "aurora-wrap-celestial.png" }],
504
543
  },
505
544
  ],
506
545
  },
507
546
  {
508
- title: 'Signature Haircut & Finish',
509
- handle: 'signature-haircut-service',
510
- sku: 'SERV-HAIR-60',
547
+ title: "Signature Haircut & Finish",
548
+ handle: "signature-haircut-service",
549
+ sku: "SERV-HAIR-60",
511
550
  description:
512
- 'Tailored haircut with relaxing wash, scalp massage, and styling finish. Designed for repeat visits in the demo portal.',
513
- categorySlug: 'services-hairdresser',
514
- customFieldsetCode: 'service_schedule',
515
- variantFieldsetCode: 'service_schedule',
516
- unit: 'hour',
517
- metadata: { channel: 'salon', serviceType: 'hairdresser' },
551
+ "Tailored haircut with relaxing wash, scalp massage, and styling finish. Designed for repeat visits in the demo portal.",
552
+ categorySlug: "services-hairdresser",
553
+ customFieldsetCode: "service_schedule",
554
+ variantFieldsetCode: "service_schedule",
555
+ unit: "hour",
556
+ metadata: { channel: "salon", serviceType: "hairdresser" },
518
557
  customFields: {
519
558
  service_duration_minutes: 60,
520
559
  service_buffer_minutes: 15,
521
- service_location: 'Salon Studio 3',
522
- service_resources: 'stylist,wash_station',
560
+ service_location: "Salon Studio 3",
561
+ service_resources: "stylist,wash_station",
523
562
  service_remote_available: false,
524
563
  },
525
- media: [{ file: 'hairdresser-service.png' }],
564
+ media: [{ file: "hairdresser-service.png" }],
526
565
  variants: [
527
566
  {
528
- name: 'Senior Stylist · 60 min',
529
- sku: 'SERV-HAIR-60-SENIOR',
567
+ name: "Senior Stylist · 60 min",
568
+ sku: "SERV-HAIR-60-SENIOR",
530
569
  isDefault: true,
531
- optionValues: { stylist: 'Senior', duration: '60' },
570
+ optionValues: { stylist: "Senior", duration: "60" },
532
571
  prices: { regular: 95, sale: 85 },
533
572
  customFields: {
534
- provider_level: 'senior',
535
- staff_member: 'Amelia Hart',
536
- environment_type: 'studio',
573
+ provider_level: "senior",
574
+ staff_member: "Amelia Hart",
575
+ environment_type: "studio",
537
576
  },
538
- media: [{ file: 'hairdresser-service.png' }],
577
+ media: [{ file: "hairdresser-service.png" }],
539
578
  },
540
579
  ],
541
580
  },
542
581
  {
543
- title: 'Restorative Massage Session',
544
- handle: 'restorative-massage-service',
545
- sku: 'SERV-MASSAGE-90',
582
+ title: "Restorative Massage Session",
583
+ handle: "restorative-massage-service",
584
+ sku: "SERV-MASSAGE-90",
546
585
  description:
547
- 'Full-body massage with aromatherapy oils and guided breathing. Includes complimentary refreshments and studio amenities.',
548
- categorySlug: 'services-massage',
549
- customFieldsetCode: 'service_schedule',
550
- variantFieldsetCode: 'service_schedule',
551
- unit: 'hour',
552
- metadata: { channel: 'wellness', serviceType: 'massage' },
586
+ "Full-body massage with aromatherapy oils and guided breathing. Includes complimentary refreshments and studio amenities.",
587
+ categorySlug: "services-massage",
588
+ customFieldsetCode: "service_schedule",
589
+ variantFieldsetCode: "service_schedule",
590
+ unit: "hour",
591
+ metadata: { channel: "wellness", serviceType: "massage" },
553
592
  customFields: {
554
593
  service_duration_minutes: 90,
555
594
  service_buffer_minutes: 20,
556
- service_location: 'Wellness Suite B',
557
- service_resources: 'therapist,treatment_room,steam_room',
595
+ service_location: "Wellness Suite B",
596
+ service_resources: "therapist,treatment_room,steam_room",
558
597
  service_remote_available: false,
559
598
  },
560
- media: [{ file: 'massage-service.png' }],
599
+ media: [{ file: "massage-service.png" }],
561
600
  variants: [
562
601
  {
563
- name: 'Master Therapist · 90 min',
564
- sku: 'SERV-MASSAGE-90-MASTER',
602
+ name: "Master Therapist · 90 min",
603
+ sku: "SERV-MASSAGE-90-MASTER",
565
604
  isDefault: true,
566
- optionValues: { therapist: 'Master', duration: '90' },
605
+ optionValues: { therapist: "Master", duration: "90" },
567
606
  prices: { regular: 140, sale: 120 },
568
607
  customFields: {
569
- provider_level: 'master',
570
- staff_member: 'Noah Li',
571
- environment_type: 'suite',
608
+ provider_level: "master",
609
+ staff_member: "Noah Li",
610
+ environment_type: "suite",
572
611
  },
573
- media: [{ file: 'massage-service.png' }],
612
+ media: [{ file: "massage-service.png" }],
574
613
  },
575
614
  ],
576
615
  },
577
- ]
616
+ ];
578
617
 
579
618
  const CHANNEL_DEFINITION = {
580
- code: 'fashion-online',
581
- name: 'Mercato Fashion Online',
582
- description: 'Direct-to-consumer storefront showcasing premium demos.',
583
- websiteUrl: 'https://demo.open-mercato.com',
584
- contactEmail: 'store@open-mercato.com',
585
- }
619
+ code: "fashion-online",
620
+ name: "Mercato Fashion Online",
621
+ description: "Direct-to-consumer storefront showcasing premium demos.",
622
+ websiteUrl: "https://demo.open-mercato.com",
623
+ contactEmail: "store@open-mercato.com",
624
+ };
586
625
 
587
626
  function formatMoney(value: number): string {
588
- return value.toFixed(2)
627
+ return value.toFixed(2);
589
628
  }
590
629
 
591
630
  async function resolveDefaultTaxRate(
592
631
  em: EntityManager,
593
- scope: SeedScope
632
+ scope: SeedScope,
594
633
  ): Promise<SalesTaxRate | null> {
595
634
  const [rate] = await em.find(
596
635
  SalesTaxRate,
@@ -602,28 +641,28 @@ async function resolveDefaultTaxRate(
602
641
  {
603
642
  limit: 1,
604
643
  orderBy: {
605
- isDefault: 'DESC',
606
- priority: 'ASC',
607
- rate: 'DESC',
608
- createdAt: 'ASC',
644
+ isDefault: "DESC",
645
+ priority: "ASC",
646
+ rate: "DESC",
647
+ createdAt: "ASC",
609
648
  },
610
- }
611
- )
612
- return rate ?? null
649
+ },
650
+ );
651
+ return rate ?? null;
613
652
  }
614
653
 
615
654
  async function ensureFieldsetConfig(
616
655
  em: EntityManager,
617
656
  scope: SeedScope,
618
657
  entityId: string,
619
- fieldsets: typeof PRODUCT_FIELDSETS | typeof VARIANT_FIELDSETS
658
+ fieldsets: typeof PRODUCT_FIELDSETS | typeof VARIANT_FIELDSETS,
620
659
  ): Promise<void> {
621
- const now = new Date()
660
+ const now = new Date();
622
661
  let config = await em.findOne(CustomFieldEntityConfig, {
623
662
  entityId,
624
663
  organizationId: scope.organizationId,
625
664
  tenantId: scope.tenantId,
626
- })
665
+ });
627
666
  if (!config) {
628
667
  config = em.create(CustomFieldEntityConfig, {
629
668
  id: randomUUID(),
@@ -633,40 +672,56 @@ async function ensureFieldsetConfig(
633
672
  isActive: true,
634
673
  createdAt: now,
635
674
  updatedAt: now,
636
- })
675
+ });
637
676
  }
638
677
  config.configJson = {
639
678
  fieldsets,
640
679
  singleFieldsetPerRecord: true,
641
- }
642
- config.isActive = true
643
- config.updatedAt = now
644
- em.persist(config)
680
+ };
681
+ config.isActive = true;
682
+ config.updatedAt = now;
683
+ em.persist(config);
645
684
  }
646
685
 
647
- async function ensureFieldsetsAndDefinitions(em: EntityManager, scope: SeedScope): Promise<void> {
648
- await ensureFieldsetConfig(em, scope, E.catalog.catalog_product, PRODUCT_FIELDSETS)
649
- await ensureFieldsetConfig(em, scope, E.catalog.catalog_product_variant, VARIANT_FIELDSETS)
686
+ async function ensureFieldsetsAndDefinitions(
687
+ em: EntityManager,
688
+ scope: SeedScope,
689
+ ): Promise<void> {
690
+ await ensureFieldsetConfig(
691
+ em,
692
+ scope,
693
+ E.catalog.catalog_product,
694
+ PRODUCT_FIELDSETS,
695
+ );
696
+ await ensureFieldsetConfig(
697
+ em,
698
+ scope,
699
+ E.catalog.catalog_product_variant,
700
+ VARIANT_FIELDSETS,
701
+ );
650
702
  await ensureCustomFieldDefinitions(em, CUSTOM_FIELD_SETS, {
651
703
  organizationId: scope.organizationId,
652
704
  tenantId: scope.tenantId,
653
- })
654
- await em.flush()
705
+ });
706
+ await em.flush();
655
707
  }
656
708
 
657
709
  async function ensureCategories(
658
710
  em: EntityManager,
659
- scope: SeedScope
711
+ scope: SeedScope,
660
712
  ): Promise<Map<string, CatalogProductCategory>> {
661
- const map = new Map<string, CatalogProductCategory>()
662
- const now = new Date()
713
+ const map = new Map<string, CatalogProductCategory>();
714
+ const now = new Date();
663
715
 
664
- const upsert = async (seed: CategorySeed, parent: CatalogProductCategory | null) => {
716
+ const upsert = async (
717
+ seed: CategorySeed,
718
+ parent: CatalogProductCategory | null,
719
+ ) => {
665
720
  let record = await em.findOne(CatalogProductCategory, {
666
721
  tenantId: scope.tenantId,
667
722
  organizationId: scope.organizationId,
668
723
  slug: seed.slug,
669
- })
724
+ });
670
725
  if (!record) {
671
726
  record = em.create(CatalogProductCategory, {
672
727
  id: randomUUID(),
@@ -676,7 +731,7 @@ async function ensureCategories(
676
731
  slug: seed.slug,
677
732
  description: seed.description ?? null,
678
733
  parentId: parent ? parent.id : null,
679
- rootId: parent ? parent.rootId ?? parent.id : null,
734
+ rootId: parent ? (parent.rootId ?? parent.id) : null,
680
735
  treePath: null,
681
736
  depth: parent ? (parent.depth ?? 0) + 1 : 0,
682
737
  ancestorIds: [],
@@ -686,41 +741,48 @@ async function ensureCategories(
686
741
  isActive: true,
687
742
  createdAt: now,
688
743
  updatedAt: now,
689
- })
690
- em.persist(record)
744
+ });
745
+ em.persist(record);
691
746
  } else {
692
- record.name = seed.name
693
- record.description = seed.description ?? null
694
- record.parentId = parent ? parent.id : null
695
- record.isActive = true
696
- record.updatedAt = now
747
+ record.name = seed.name;
748
+ record.description = seed.description ?? null;
749
+ record.parentId = parent ? parent.id : null;
750
+ record.isActive = true;
751
+ record.updatedAt = now;
697
752
  }
698
- map.set(seed.slug, record)
753
+ map.set(seed.slug, record);
699
754
  if (Array.isArray(seed.children)) {
700
755
  for (const child of seed.children) {
701
- await upsert(child, record)
756
+ await upsert(child, record);
702
757
  }
703
758
  }
704
- }
759
+ };
705
760
 
706
761
  for (const seed of CATEGORY_TREE) {
707
- await upsert(seed, null)
762
+ await upsert(seed, null);
708
763
  }
709
764
 
710
- await em.flush()
711
- await rebuildCategoryHierarchyForOrganization(em, scope.organizationId, scope.tenantId)
765
+ await em.flush();
766
+ await rebuildCategoryHierarchyForOrganization(
767
+ em,
768
+ scope.organizationId,
769
+ scope.tenantId,
770
+ );
712
771
 
713
- return map
772
+ return map;
714
773
  }
715
774
 
716
- async function ensureChannel(em: EntityManager, scope: SeedScope): Promise<SalesChannel> {
717
- const now = new Date()
775
+ async function ensureChannel(
776
+ em: EntityManager,
777
+ scope: SeedScope,
778
+ ): Promise<SalesChannel> {
779
+ const now = new Date();
718
780
  let channel = await em.findOne(SalesChannel, {
719
781
  tenantId: scope.tenantId,
720
782
  organizationId: scope.organizationId,
721
783
  code: CHANNEL_DEFINITION.code,
722
784
  deletedAt: null,
723
- })
785
+ });
724
786
  if (!channel) {
725
787
  channel = em.create(SalesChannel, {
726
788
  id: randomUUID(),
@@ -731,72 +793,76 @@ async function ensureChannel(em: EntityManager, scope: SeedScope): Promise<Sales
731
793
  description: CHANNEL_DEFINITION.description,
732
794
  websiteUrl: CHANNEL_DEFINITION.websiteUrl,
733
795
  contactEmail: CHANNEL_DEFINITION.contactEmail,
734
- status: 'active',
796
+ status: "active",
735
797
  isActive: true,
736
- metadata: { locale: 'en-US' },
798
+ metadata: { locale: "en-US" },
737
799
  createdAt: now,
738
800
  updatedAt: now,
739
- })
740
- em.persist(channel)
741
- await em.flush()
801
+ });
802
+ em.persist(channel);
803
+ await em.flush();
742
804
  }
743
- return channel
805
+ return channel;
744
806
  }
745
807
 
746
808
  async function loadPriceKinds(
747
809
  em: EntityManager,
748
- scope: SeedScope
810
+ scope: SeedScope,
749
811
  ): Promise<Map<string, CatalogPriceKind>> {
750
812
  const kinds = await em.find(CatalogPriceKind, {
751
813
  tenantId: scope.tenantId,
752
- code: { $in: ['regular', 'sale'] },
814
+ code: { $in: ["regular", "sale"] },
753
815
  deletedAt: null,
754
- })
755
- const map = new Map<string, CatalogPriceKind>()
816
+ });
817
+ const map = new Map<string, CatalogPriceKind>();
756
818
  for (const kind of kinds) {
757
- map.set(kind.code.toLowerCase(), kind)
819
+ map.set(kind.code.toLowerCase(), kind);
758
820
  }
759
- return map
821
+ return map;
760
822
  }
761
823
 
762
824
  export async function seedCatalogExamples(
763
825
  em: EntityManager,
764
826
  container: AwilixContainer,
765
- scope: SeedScope
827
+ scope: SeedScope,
766
828
  ): Promise<boolean> {
767
- await ensureFieldsetsAndDefinitions(em, scope)
768
- await ensureDefaultPartitions(em)
829
+ await ensureFieldsetsAndDefinitions(em, scope);
830
+ await ensureDefaultPartitions(em);
769
831
 
770
- const handles = PRODUCT_SEEDS.map((seed) => seed.handle)
832
+ const handles = PRODUCT_SEEDS.map((seed) => seed.handle);
771
833
  const existingProducts = await em.find(CatalogProduct, {
772
834
  tenantId: scope.tenantId,
773
835
  organizationId: scope.organizationId,
774
- handle: { $in: handles as any },
775
- })
776
- const existingByHandle = new Map(existingProducts.map((product) => [product.handle?.toLowerCase(), product]))
777
-
778
- const categoryMap = await ensureCategories(em, scope)
779
- const channel = await ensureChannel(em, scope)
780
- const priceKinds = await loadPriceKinds(em, scope)
781
- const regularKind = priceKinds.get('regular')
782
- const saleKind = priceKinds.get('sale')
783
- const defaultTaxRate = await resolveDefaultTaxRate(em, scope)
784
- const defaultTaxRateId = defaultTaxRate?.id ?? null
785
- const defaultTaxRateValue = defaultTaxRate?.rate ?? null
836
+ handle: { $in: [...handles] },
837
+ });
838
+ const existingByHandle = new Map(
839
+ existingProducts.map((product) => [product.handle?.toLowerCase(), product]),
840
+ );
841
+
842
+ const categoryMap = await ensureCategories(em, scope);
843
+ const channel = await ensureChannel(em, scope);
844
+ const priceKinds = await loadPriceKinds(em, scope);
845
+ const regularKind = priceKinds.get("regular");
846
+ const saleKind = priceKinds.get("sale");
847
+ const defaultTaxRate = await resolveDefaultTaxRate(em, scope);
848
+ const defaultTaxRateId = defaultTaxRate?.id ?? null;
849
+ const defaultTaxRateValue = defaultTaxRate?.rate ?? null;
786
850
  if (!regularKind || !saleKind) {
787
- throw new Error('Missing catalog price kinds; run `mercato catalog seed-price-kinds` first.')
851
+ throw new Error(
852
+ "Missing catalog price kinds; run `mercato catalog seed-price-kinds` first.",
853
+ );
788
854
  }
789
855
 
790
- const dataEngine = new DefaultDataEngine(em, container)
791
- const customFieldAssignments: Array<() => Promise<void>> = []
792
- let createdAny = false
856
+ const dataEngine = new DefaultDataEngine(em, container);
857
+ const customFieldAssignments: Array<() => Promise<void>> = [];
858
+ let createdAny = false;
793
859
 
794
860
  for (const productSeed of PRODUCT_SEEDS) {
795
- const existing = existingByHandle.get(productSeed.handle.toLowerCase())
861
+ const existing = existingByHandle.get(productSeed.handle.toLowerCase());
796
862
  if (existing) {
797
- continue
863
+ continue;
798
864
  }
799
- createdAny = true
865
+ createdAny = true;
800
866
  const product = em.create(CatalogProduct, {
801
867
  id: randomUUID(),
802
868
  organizationId: scope.organizationId,
@@ -805,9 +871,9 @@ export async function seedCatalogExamples(
805
871
  description: productSeed.description,
806
872
  sku: productSeed.sku ?? null,
807
873
  handle: productSeed.handle,
808
- productType: 'configurable',
809
- primaryCurrencyCode: 'USD',
810
- defaultUnit: productSeed.unit,
874
+ productType: "configurable",
875
+ primaryCurrencyCode: "USD",
876
+ defaultUnit: canonicalizeUnitCode(productSeed.unit) ?? productSeed.unit,
811
877
  customFieldsetCode: productSeed.customFieldsetCode,
812
878
  metadata: productSeed.metadata ?? null,
813
879
  taxRateId: defaultTaxRateId,
@@ -816,10 +882,10 @@ export async function seedCatalogExamples(
816
882
  isActive: true,
817
883
  createdAt: new Date(),
818
884
  updatedAt: new Date(),
819
- })
820
- em.persist(product)
885
+ });
886
+ em.persist(product);
821
887
 
822
- const category = categoryMap.get(productSeed.categorySlug)
888
+ const category = categoryMap.get(productSeed.categorySlug);
823
889
  if (category) {
824
890
  const assignment = em.create(CatalogProductCategoryAssignment, {
825
891
  id: randomUUID(),
@@ -830,8 +896,8 @@ export async function seedCatalogExamples(
830
896
  position: 0,
831
897
  createdAt: new Date(),
832
898
  updatedAt: new Date(),
833
- })
834
- em.persist(assignment)
899
+ });
900
+ em.persist(assignment);
835
901
  }
836
902
 
837
903
  const offer = em.create(CatalogOffer, {
@@ -841,16 +907,19 @@ export async function seedCatalogExamples(
841
907
  product,
842
908
  channelId: channel.id,
843
909
  title: `${productSeed.title} · Online`,
844
- description: 'Offer curated for the demo storefront channel.',
910
+ description: "Offer curated for the demo storefront channel.",
845
911
  metadata: { channelCode: CHANNEL_DEFINITION.code },
846
912
  isActive: true,
847
913
  createdAt: new Date(),
848
914
  updatedAt: new Date(),
849
- })
850
- em.persist(offer)
915
+ });
916
+ em.persist(offer);
851
917
 
852
- if (productSeed.customFields && Object.keys(productSeed.customFields).length) {
853
- const payload = { ...productSeed.customFields }
918
+ if (
919
+ productSeed.customFields &&
920
+ Object.keys(productSeed.customFields).length
921
+ ) {
922
+ const payload = { ...productSeed.customFields };
854
923
  customFieldAssignments.push(() =>
855
924
  dataEngine.setCustomFields({
856
925
  entityId: E.catalog.catalog_product,
@@ -858,8 +927,8 @@ export async function seedCatalogExamples(
858
927
  organizationId: scope.organizationId,
859
928
  tenantId: scope.tenantId,
860
929
  values: payload,
861
- })
862
- )
930
+ }),
931
+ );
863
932
  }
864
933
 
865
934
  const productMedia = await attachMediaFromExamples(
@@ -867,14 +936,14 @@ export async function seedCatalogExamples(
867
936
  scope,
868
937
  E.catalog.catalog_product,
869
938
  product.id,
870
- productSeed.media
871
- )
939
+ productSeed.media,
940
+ );
872
941
  if (productMedia.length) {
873
- const hero = productMedia[0]
874
- product.defaultMediaId = hero.id
875
- product.defaultMediaUrl = hero.imageUrl
876
- offer.defaultMediaId = hero.id
877
- offer.defaultMediaUrl = hero.imageUrl
942
+ const hero = productMedia[0];
943
+ product.defaultMediaId = hero.id;
944
+ product.defaultMediaUrl = hero.imageUrl;
945
+ offer.defaultMediaId = hero.id;
946
+ offer.defaultMediaUrl = hero.imageUrl;
878
947
  }
879
948
 
880
949
  for (const variantSeed of productSeed.variants) {
@@ -894,18 +963,16 @@ export async function seedCatalogExamples(
894
963
  isActive: true,
895
964
  createdAt: new Date(),
896
965
  updatedAt: new Date(),
897
- })
898
- em.persist(variant)
966
+ });
967
+ em.persist(variant);
899
968
 
900
969
  const variantMedia = await attachMediaFromExamples(
901
970
  em,
902
971
  scope,
903
972
  E.catalog.catalog_product_variant,
904
973
  variant.id,
905
- variantSeed.media
906
- )
907
- const variantCover = variantMedia[0] ?? productMedia[0]
908
-
974
+ variantSeed.media,
975
+ );
909
976
  const regularPrice = em.create(CatalogProductPrice, {
910
977
  id: randomUUID(),
911
978
  organizationId: scope.organizationId,
@@ -914,7 +981,7 @@ export async function seedCatalogExamples(
914
981
  variant,
915
982
  offer,
916
983
  priceKind: regularKind,
917
- currencyCode: 'USD',
984
+ currencyCode: "USD",
918
985
  kind: regularKind.code,
919
986
  minQuantity: 1,
920
987
  taxRate: defaultTaxRateValue,
@@ -923,8 +990,8 @@ export async function seedCatalogExamples(
923
990
  channelId: channel.id,
924
991
  createdAt: new Date(),
925
992
  updatedAt: new Date(),
926
- })
927
- em.persist(regularPrice)
993
+ });
994
+ em.persist(regularPrice);
928
995
 
929
996
  if (variantSeed.prices.sale !== undefined) {
930
997
  const salePrice = em.create(CatalogProductPrice, {
@@ -935,7 +1002,7 @@ export async function seedCatalogExamples(
935
1002
  variant,
936
1003
  offer,
937
1004
  priceKind: saleKind,
938
- currencyCode: 'USD',
1005
+ currencyCode: "USD",
939
1006
  kind: saleKind.code,
940
1007
  minQuantity: 1,
941
1008
  taxRate: defaultTaxRateValue,
@@ -944,12 +1011,15 @@ export async function seedCatalogExamples(
944
1011
  channelId: channel.id,
945
1012
  createdAt: new Date(),
946
1013
  updatedAt: new Date(),
947
- })
948
- em.persist(salePrice)
1014
+ });
1015
+ em.persist(salePrice);
949
1016
  }
950
1017
 
951
- if (variantSeed.customFields && Object.keys(variantSeed.customFields).length) {
952
- const payload = { ...variantSeed.customFields }
1018
+ if (
1019
+ variantSeed.customFields &&
1020
+ Object.keys(variantSeed.customFields).length
1021
+ ) {
1022
+ const payload = { ...variantSeed.customFields };
953
1023
  customFieldAssignments.push(() =>
954
1024
  dataEngine.setCustomFields({
955
1025
  entityId: E.catalog.catalog_product_variant,
@@ -957,25 +1027,28 @@ export async function seedCatalogExamples(
957
1027
  organizationId: scope.organizationId,
958
1028
  tenantId: scope.tenantId,
959
1029
  values: payload,
960
- })
961
- )
1030
+ }),
1031
+ );
962
1032
  }
963
1033
  }
964
1034
  }
965
1035
 
966
1036
  if (!createdAny) {
967
- return false
1037
+ return false;
968
1038
  }
969
1039
 
970
- await em.flush()
1040
+ await em.flush();
971
1041
 
972
1042
  for (const assign of customFieldAssignments) {
973
1043
  try {
974
- await assign()
1044
+ await assign();
975
1045
  } catch (err) {
976
- console.warn('[catalog.seed] Failed to set example custom field values', err)
1046
+ console.warn(
1047
+ "[catalog.seed] Failed to set example custom field values",
1048
+ err,
1049
+ );
977
1050
  }
978
1051
  }
979
1052
 
980
- return true
1053
+ return true;
981
1054
  }