@open-mercato/core 0.9.1-develop.7498.1.08d233a6cc → 0.9.1-develop.7513.1.ae2715553f

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 (68) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/audit_logs/components/AuditLogsActions.js +1 -1
  3. package/dist/modules/audit_logs/components/AuditLogsActions.js.map +2 -2
  4. package/dist/modules/auth/frontend/reset.js +7 -3
  5. package/dist/modules/auth/frontend/reset.js.map +2 -2
  6. package/dist/modules/business_rules/api/rules/route.js +12 -1
  7. package/dist/modules/business_rules/api/rules/route.js.map +2 -2
  8. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +17 -2
  9. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  10. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +3 -2
  11. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
  12. package/dist/modules/catalog/backend/catalog/products/create/page.js +1 -0
  13. package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
  14. package/dist/modules/catalog/components/products/productFormUtils.js +9 -0
  15. package/dist/modules/catalog/components/products/productFormUtils.js.map +2 -2
  16. package/dist/modules/customers/backend/customers/companies/page.js +32 -32
  17. package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
  18. package/dist/modules/customers/backend/customers/deals/page.js +22 -22
  19. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  20. package/dist/modules/customers/backend/customers/people/page.js +34 -34
  21. package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
  22. package/dist/modules/ecommerce/search.js +1 -2
  23. package/dist/modules/ecommerce/search.js.map +2 -2
  24. package/dist/modules/staff/backend/staff/time-tracking/entries/page.js +10 -7
  25. package/dist/modules/staff/backend/staff/time-tracking/entries/page.js.map +2 -2
  26. package/dist/modules/wms/api/interceptors.js +3 -0
  27. package/dist/modules/wms/api/interceptors.js.map +2 -2
  28. package/dist/modules/wms/commands/configuration.js +16 -16
  29. package/dist/modules/wms/commands/configuration.js.map +2 -2
  30. package/dist/modules/wms/events.js +1 -0
  31. package/dist/modules/wms/events.js.map +2 -2
  32. package/dist/modules/wms/lib/catalogInventoryProfileIntent.js +9 -0
  33. package/dist/modules/wms/lib/catalogInventoryProfileIntent.js.map +2 -2
  34. package/dist/modules/wms/widgets/injection/catalog-inventory-profile/widget.js +11 -3
  35. package/dist/modules/wms/widgets/injection/catalog-inventory-profile/widget.js.map +2 -2
  36. package/package.json +7 -7
  37. package/src/modules/audit_logs/components/AuditLogsActions.tsx +1 -1
  38. package/src/modules/auth/frontend/reset.tsx +8 -3
  39. package/src/modules/business_rules/api/rules/route.ts +11 -0
  40. package/src/modules/business_rules/i18n/de.json +1 -0
  41. package/src/modules/business_rules/i18n/en.json +1 -0
  42. package/src/modules/business_rules/i18n/es.json +1 -0
  43. package/src/modules/business_rules/i18n/ko.json +1 -0
  44. package/src/modules/business_rules/i18n/pl.json +1 -0
  45. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +20 -1
  46. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +2 -1
  47. package/src/modules/catalog/backend/catalog/products/create/page.tsx +1 -0
  48. package/src/modules/catalog/components/products/productFormUtils.ts +9 -0
  49. package/src/modules/customers/backend/customers/companies/page.tsx +32 -32
  50. package/src/modules/customers/backend/customers/deals/page.tsx +22 -22
  51. package/src/modules/customers/backend/customers/people/page.tsx +34 -34
  52. package/src/modules/customers/i18n/de.json +12 -0
  53. package/src/modules/customers/i18n/en.json +12 -0
  54. package/src/modules/customers/i18n/es.json +12 -0
  55. package/src/modules/customers/i18n/ko.json +12 -0
  56. package/src/modules/customers/i18n/pl.json +12 -0
  57. package/src/modules/ecommerce/i18n/de.json +2 -1
  58. package/src/modules/ecommerce/i18n/en.json +2 -1
  59. package/src/modules/ecommerce/i18n/es.json +2 -1
  60. package/src/modules/ecommerce/i18n/ko.json +2 -1
  61. package/src/modules/ecommerce/i18n/pl.json +2 -1
  62. package/src/modules/ecommerce/search.ts +0 -1
  63. package/src/modules/staff/backend/staff/time-tracking/entries/page.tsx +20 -14
  64. package/src/modules/wms/api/interceptors.ts +9 -0
  65. package/src/modules/wms/commands/configuration.ts +28 -16
  66. package/src/modules/wms/events.ts +1 -0
  67. package/src/modules/wms/lib/catalogInventoryProfileIntent.ts +9 -0
  68. package/src/modules/wms/widgets/injection/catalog-inventory-profile/widget.ts +13 -4
@@ -337,5 +337,6 @@
337
337
  "ecommerce.validation.pathPrefixInvalid": "Path prefix may contain only lowercase letters, digits, dashes and slashes, for example /shop.",
338
338
  "ecommerce.validation.slugInvalid": "Slug may contain only lowercase letters, digits and single dashes between them.",
339
339
  "ecommerce.validation.urlInvalid": "Enter a valid http(s) URL or a path starting with /.",
340
- "notifications.categories.ecommerce": "Ecommerce"
340
+ "notifications.categories.ecommerce": "Ecommerce",
341
+ "search.entityType.ecommerce.ecommerce_store": "Store"
341
342
  }
@@ -337,5 +337,6 @@
337
337
  "ecommerce.validation.pathPrefixInvalid": "El prefijo de ruta solo puede contener letras minúsculas, dígitos, guiones y barras, por ejemplo /shop.",
338
338
  "ecommerce.validation.slugInvalid": "El slug solo puede contener letras minúsculas, dígitos y guiones simples entre ellos.",
339
339
  "ecommerce.validation.urlInvalid": "Introduce una URL http(s) válida o una ruta que empiece por /.",
340
- "notifications.categories.ecommerce": "Comercio electrónico"
340
+ "notifications.categories.ecommerce": "Comercio electrónico",
341
+ "search.entityType.ecommerce.ecommerce_store": "Tienda"
341
342
  }
@@ -337,5 +337,6 @@
337
337
  "ecommerce.validation.pathPrefixInvalid": "경로 접두사에는 영문 소문자, 숫자, 하이픈, 슬래시만 사용할 수 있습니다(예: /shop).",
338
338
  "ecommerce.validation.slugInvalid": "슬러그에는 영문 소문자, 숫자와 그 사이의 단일 하이픈만 사용할 수 있습니다.",
339
339
  "ecommerce.validation.urlInvalid": "유효한 http(s) URL 또는 /로 시작하는 경로를 입력하세요.",
340
- "notifications.categories.ecommerce": "이커머스"
340
+ "notifications.categories.ecommerce": "이커머스",
341
+ "search.entityType.ecommerce.ecommerce_store": "스토어"
341
342
  }
@@ -337,5 +337,6 @@
337
337
  "ecommerce.validation.pathPrefixInvalid": "Prefiks ścieżki może zawierać tylko małe litery, cyfry, myślniki i ukośniki, np. /shop.",
338
338
  "ecommerce.validation.slugInvalid": "Slug może zawierać tylko małe litery, cyfry i pojedyncze myślniki między nimi.",
339
339
  "ecommerce.validation.urlInvalid": "Podaj prawidłowy adres URL http(s) lub ścieżkę zaczynającą się od /.",
340
- "notifications.categories.ecommerce": "E-commerce"
340
+ "notifications.categories.ecommerce": "E-commerce",
341
+ "search.entityType.ecommerce.ecommerce_store": "Sklep"
341
342
  }
@@ -39,7 +39,6 @@ function buildStorePresenter(
39
39
  title,
40
40
  subtitle: subtitleParts.length ? subtitleParts.join(' · ') : undefined,
41
41
  icon: 'store',
42
- badge,
43
42
  }
44
43
  }
45
44
 
@@ -353,7 +353,7 @@ export default function TimeTrackingEntriesPage() {
353
353
  }
354
354
  }
355
355
  const [selfRes, projectsRes, tagsRes, peopleRes] = await Promise.all([
356
- safeCall<{ member?: { id?: string } | null }>('/api/staff/team-members/self'),
356
+ safeCall<{ member?: { id?: string; displayName?: string } | null }>('/api/staff/team-members/self'),
357
357
  safeCall<{ items?: Array<Record<string, unknown>> }>('/api/staff/timesheets/time-projects?page=1&pageSize=100'),
358
358
  safeCall<{ items?: Array<Record<string, unknown>> }>('/api/staff/timesheets/tags?page=1&pageSize=100'),
359
359
  safeCall<{ items?: Array<Record<string, unknown>> }>('/api/staff/team-members?page=1&pageSize=100'),
@@ -399,19 +399,25 @@ export default function TimeTrackingEntriesPage() {
399
399
  )
400
400
 
401
401
  const people = peopleRes?.ok && Array.isArray(peopleRes.result?.items) ? peopleRes.result.items : []
402
- setPeopleOptions(
403
- people
404
- .map((row: Record<string, unknown>) => {
405
- const id = typeof row.id === 'string' ? row.id : null
406
- const label = typeof row.display_name === 'string'
407
- ? row.display_name
408
- : typeof row.displayName === 'string'
409
- ? row.displayName
410
- : null
411
- return id && label ? { value: id, label } : null
412
- })
413
- .filter((option): option is { value: string; label: string } => option !== null),
414
- )
402
+ const personOptions = people
403
+ .map((row: Record<string, unknown>) => {
404
+ const id = typeof row.id === 'string' ? row.id : null
405
+ const label = typeof row.display_name === 'string'
406
+ ? row.display_name
407
+ : typeof row.displayName === 'string'
408
+ ? row.displayName
409
+ : null
410
+ return id && label ? { value: id, label } : null
411
+ })
412
+ .filter((option): option is { value: string; label: string } => option !== null)
413
+ // The team list is closed to a caller without team-member access, yet the
414
+ // self filter is seeded for everyone — name it from the self lookup so its
415
+ // chip never falls back to the raw member id.
416
+ const selfName = selfRes?.ok ? selfRes.result?.member?.displayName : undefined
417
+ if (selfId && selfName && !personOptions.some((option) => option.value === selfId)) {
418
+ personOptions.push({ value: selfId, label: selfName })
419
+ }
420
+ setPeopleOptions(personOptions)
415
421
  })()
416
422
  return () => { cancelled = true }
417
423
  }, [scopeVersion])
@@ -1,4 +1,5 @@
1
1
  import type { ApiInterceptor } from '@open-mercato/shared/lib/crud/api-interceptor'
2
+ import { E } from '#generated/entities.ids.generated'
2
3
  import {
3
4
  decodeCatalogInventoryProfileIntent,
4
5
  WMS_CATALOG_PROFILE_HEADER,
@@ -35,6 +36,12 @@ export const interceptors: ApiInterceptor[] = [
35
36
  if (!rawHeader) return { ok: true }
36
37
 
37
38
  const intent = decodeCatalogInventoryProfileIntent(rawHeader)
39
+ // CrudForm scopes `onBeforeSave` request headers around the whole
40
+ // `onSubmit` callback, so a page whose submit handler also creates
41
+ // other records (e.g. the product-create wizard implicitly creating
42
+ // its default variant) can leak this header onto those unrelated
43
+ // requests. Only honor an intent the product form itself computed.
44
+ if (intent?.sourceEntityId !== E.catalog.catalog_product) return { ok: true }
38
45
  return {
39
46
  ok: true,
40
47
  metadata: { intent, target: 'product' },
@@ -74,6 +81,8 @@ export const interceptors: ApiInterceptor[] = [
74
81
  if (!rawHeader) return { ok: true }
75
82
 
76
83
  const intent = decodeCatalogInventoryProfileIntent(rawHeader)
84
+ // See the product interceptor's `before` for why this check exists.
85
+ if (intent?.sourceEntityId !== E.catalog.catalog_product_variant) return { ok: true }
77
86
  return {
78
87
  ok: true,
79
88
  metadata: { intent, target: 'variant' },
@@ -1532,6 +1532,28 @@ function restoreInventoryProfileFromSnapshot(em: EntityManager, before: ProductI
1532
1532
  record.deletedAt = null
1533
1533
  }
1534
1534
 
1535
+ type InventoryProfileEventSource = {
1536
+ id: string
1537
+ catalogProductId: string
1538
+ catalogVariantId?: string | null
1539
+ tenantId: string
1540
+ organizationId: string
1541
+ }
1542
+
1543
+ function emitInventoryProfileEvent(
1544
+ eventId: 'wms.inventory_profile.created' | 'wms.inventory_profile.updated' | 'wms.inventory_profile.deleted',
1545
+ profile: InventoryProfileEventSource,
1546
+ ): void {
1547
+ void emitWmsEvent(eventId, {
1548
+ id: profile.id,
1549
+ profileId: profile.id,
1550
+ catalogProductId: profile.catalogProductId,
1551
+ catalogVariantId: profile.catalogVariantId ?? null,
1552
+ tenantId: profile.tenantId,
1553
+ organizationId: profile.organizationId,
1554
+ }).catch(() => undefined)
1555
+ }
1556
+
1535
1557
  const createProductInventoryProfileCommand: CommandHandler<ProductInventoryProfileCreateInput, { profileId: string }> = {
1536
1558
  id: 'wms.inventoryProfiles.create',
1537
1559
  async execute(rawInput, ctx) {
@@ -1555,14 +1577,7 @@ const createProductInventoryProfileCommand: CommandHandler<ProductInventoryProfi
1555
1577
  metadata: toJsonValue(parsed.metadata),
1556
1578
  })
1557
1579
  await em.persist(profile).flush()
1558
- void emitWmsEvent('wms.inventory_profile.created', {
1559
- id: profile.id,
1560
- profileId: profile.id,
1561
- catalogProductId: profile.catalogProductId,
1562
- catalogVariantId: profile.catalogVariantId ?? null,
1563
- tenantId: profile.tenantId,
1564
- organizationId: profile.organizationId,
1565
- }).catch(() => undefined)
1580
+ emitInventoryProfileEvent('wms.inventory_profile.created', profile)
1566
1581
  return { profileId: profile.id }
1567
1582
  },
1568
1583
  captureAfter: async (_input, result, ctx) => {
@@ -1591,6 +1606,7 @@ const createProductInventoryProfileCommand: CommandHandler<ProductInventoryProfi
1591
1606
  ensureOrganizationScope(ctx, record.organizationId)
1592
1607
  record.deletedAt = new Date()
1593
1608
  await em.flush()
1609
+ emitInventoryProfileEvent('wms.inventory_profile.deleted', record)
1594
1610
  },
1595
1611
  }
1596
1612
 
@@ -1629,14 +1645,7 @@ const updateProductInventoryProfileCommand: CommandHandler<ProductInventoryProfi
1629
1645
  if (parsed.safetyStock !== undefined) profile.safetyStock = toNumericString(parsed.safetyStock)
1630
1646
  if (parsed.metadata !== undefined) profile.metadata = toJsonValue(parsed.metadata)
1631
1647
  await em.flush()
1632
- void emitWmsEvent('wms.inventory_profile.updated', {
1633
- id: profile.id,
1634
- profileId: profile.id,
1635
- catalogProductId: profile.catalogProductId,
1636
- catalogVariantId: profile.catalogVariantId ?? null,
1637
- tenantId: profile.tenantId,
1638
- organizationId: profile.organizationId,
1639
- }).catch(() => undefined)
1648
+ emitInventoryProfileEvent('wms.inventory_profile.updated', profile)
1640
1649
  return { profileId: profile.id }
1641
1650
  },
1642
1651
  captureAfter: async (_input, result, ctx) => {
@@ -1686,6 +1695,7 @@ const updateProductInventoryProfileCommand: CommandHandler<ProductInventoryProfi
1686
1695
  restoreInventoryProfileFromSnapshot(em, before, record)
1687
1696
  }
1688
1697
  await em.flush()
1698
+ emitInventoryProfileEvent('wms.inventory_profile.updated', before)
1689
1699
  },
1690
1700
  }
1691
1701
 
@@ -1703,6 +1713,7 @@ const deleteProductInventoryProfileCommand: CommandHandler<{ id?: string }, { pr
1703
1713
  const profile = await loadProfile(em, ctx, profileId)
1704
1714
  profile.deletedAt = new Date()
1705
1715
  await em.flush()
1716
+ emitInventoryProfileEvent('wms.inventory_profile.deleted', profile)
1706
1717
  return { profileId: profile.id }
1707
1718
  },
1708
1719
  buildLog: async ({ input, result, ctx, snapshots }) => {
@@ -1747,6 +1758,7 @@ const deleteProductInventoryProfileCommand: CommandHandler<{ id?: string }, { pr
1747
1758
  restoreInventoryProfileFromSnapshot(em, before, record)
1748
1759
  }
1749
1760
  await em.flush()
1761
+ emitInventoryProfileEvent('wms.inventory_profile.created', before)
1750
1762
  },
1751
1763
  }
1752
1764
 
@@ -9,6 +9,7 @@ const events = [
9
9
  { id: 'wms.location.updated', label: 'Warehouse Location Updated', entity: 'location', category: 'crud' },
10
10
  { id: 'wms.inventory_profile.created', label: 'Inventory Profile Created', entity: 'inventory_profile', category: 'crud' },
11
11
  { id: 'wms.inventory_profile.updated', label: 'Inventory Profile Updated', entity: 'inventory_profile', category: 'crud' },
12
+ { id: 'wms.inventory_profile.deleted', label: 'Inventory Profile Deleted', entity: 'inventory_profile', category: 'crud' },
12
13
  { id: 'wms.inventory_balance.created', label: 'Inventory Balance Created', entity: 'inventory_balance', category: 'crud' },
13
14
  { id: 'wms.inventory_balance.updated', label: 'Inventory Balance Updated', entity: 'inventory_balance', category: 'crud' },
14
15
  { id: 'wms.inventory_balance.deleted', label: 'Inventory Balance Deleted', entity: 'inventory_balance', category: 'crud' },
@@ -3,6 +3,15 @@ import { z } from 'zod'
3
3
  const inventoryStrategySchema = z.enum(['fifo', 'lifo', 'fefo'])
4
4
 
5
5
  export const catalogInventoryProfileIntentSchema = z.object({
6
+ // Identifies which host CrudForm (product vs. variant) computed this intent.
7
+ // CrudForm scopes `onBeforeSave` request headers around the whole `onSubmit`
8
+ // callback, not around a single HTTP call, so a page whose submit handler
9
+ // creates secondary records (e.g. the product-create wizard implicitly
10
+ // creating its default variant) can leak this header onto those unrelated
11
+ // requests. Each interceptor only acts when this matches its own target
12
+ // entity, so a leaked intent is ignored rather than creating a profile the
13
+ // user never asked for on that other entity.
14
+ sourceEntityId: z.string(),
6
15
  manageInventory: z.boolean(),
7
16
  defaultUom: z.string().trim().max(32).nullable().optional(),
8
17
  defaultStrategy: inventoryStrategySchema.optional(),
@@ -1,4 +1,4 @@
1
- import type { InjectionFieldWidget } from '@open-mercato/shared/modules/widgets/injection'
1
+ import type { InjectionContext, InjectionFieldWidget } from '@open-mercato/shared/modules/widgets/injection'
2
2
  import {
3
3
  encodeCatalogInventoryProfileIntent,
4
4
  WMS_CATALOG_PROFILE_HEADER,
@@ -29,8 +29,9 @@ function readNumber(value: unknown): number | undefined {
29
29
  return undefined
30
30
  }
31
31
 
32
- function buildIntent(data: RecordLike): CatalogInventoryProfileIntent {
32
+ function buildIntent(data: RecordLike, sourceEntityId: string): CatalogInventoryProfileIntent {
33
33
  return {
34
+ sourceEntityId,
34
35
  manageInventory: readBoolean(data['wms.manageInventory']),
35
36
  defaultUom: readString(data['wms.defaultUom']).trim() || null,
36
37
  defaultStrategy: (() => {
@@ -47,6 +48,10 @@ function buildIntent(data: RecordLike): CatalogInventoryProfileIntent {
47
48
  }
48
49
  }
49
50
 
51
+ function hasLoadedInventoryProfile(data: RecordLike): boolean {
52
+ return Boolean(asRecord(asRecord(data._wms)?.inventoryProfile))
53
+ }
54
+
50
55
  function buildDisplayState(data: RecordLike): RecordLike {
51
56
  const wms = asRecord(data._wms)
52
57
  const inventoryProfile = asRecord(wms?.inventoryProfile)
@@ -170,12 +175,16 @@ const widget: InjectionFieldWidget = {
170
175
  if (!source) return data
171
176
  return buildDisplayState(source) as typeof data
172
177
  },
173
- async onBeforeSave(data) {
178
+ async onBeforeSave(data, context: InjectionContext) {
174
179
  const source = asRecord(data)
175
180
  if (!source) return { ok: true }
176
181
 
177
- const intent = buildIntent(source)
182
+ const sourceEntityId = readString(context?.entityId)
183
+ const intent = buildIntent(source, sourceEntityId)
178
184
  if (!intent.manageInventory) {
185
+ if (source['wms.manageInventory'] !== false || !hasLoadedInventoryProfile(source)) {
186
+ return { ok: true }
187
+ }
179
188
  return {
180
189
  ok: true,
181
190
  requestHeaders: {