@open-mercato/core 0.6.7-develop.6706.1.b3a4c759bb → 0.6.7-develop.6725.1.07a748cfc2

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 (111) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/admin/nav.js +14 -4
  3. package/dist/modules/auth/api/admin/nav.js.map +2 -2
  4. package/dist/modules/auth/api/users/route.js +8 -19
  5. package/dist/modules/auth/api/users/route.js.map +2 -2
  6. package/dist/modules/auth/lib/backendChrome.js +31 -14
  7. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  8. package/dist/modules/customers/api/utils.js +12 -18
  9. package/dist/modules/customers/api/utils.js.map +2 -2
  10. package/dist/modules/customers/cli.js +11 -4
  11. package/dist/modules/customers/cli.js.map +2 -2
  12. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +4 -2
  13. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  14. package/dist/modules/customers/components/formConfig.js +14 -10
  15. package/dist/modules/customers/components/formConfig.js.map +2 -2
  16. package/dist/modules/dashboards/api/widgets/data/batch/route.js +4 -2
  17. package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +2 -2
  18. package/dist/modules/dashboards/api/widgets/data/route.js +20 -2
  19. package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
  20. package/dist/modules/dashboards/lib/aggregations.js +76 -40
  21. package/dist/modules/dashboards/lib/aggregations.js.map +2 -2
  22. package/dist/modules/dashboards/lib/exactDecimal.js +69 -0
  23. package/dist/modules/dashboards/lib/exactDecimal.js.map +7 -0
  24. package/dist/modules/dashboards/services/widgetDataService.js +228 -5
  25. package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
  26. package/dist/modules/feature_toggles/components/FeatureGuard.js +3 -2
  27. package/dist/modules/feature_toggles/components/FeatureGuard.js.map +2 -2
  28. package/dist/modules/feature_toggles/components/hooks/staleTime.js +5 -0
  29. package/dist/modules/feature_toggles/components/hooks/staleTime.js.map +7 -0
  30. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.js +5 -2
  31. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.js.map +2 -2
  32. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagJson.js +5 -2
  33. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagJson.js.map +2 -2
  34. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagNumber.js +5 -2
  35. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagNumber.js.map +2 -2
  36. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagString.js +5 -2
  37. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagString.js.map +2 -2
  38. package/dist/modules/inbox_ops/api/proposals/route.js +16 -1
  39. package/dist/modules/inbox_ops/api/proposals/route.js.map +2 -2
  40. package/dist/modules/inbox_ops/subscribers/search-reindex-proposal-created.js +32 -0
  41. package/dist/modules/inbox_ops/subscribers/search-reindex-proposal-created.js.map +7 -0
  42. package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js +0 -5
  43. package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js.map +2 -2
  44. package/dist/modules/integrations/widgets/injection/external-ids/widget.js +14 -0
  45. package/dist/modules/integrations/widgets/injection/external-ids/widget.js.map +7 -0
  46. package/dist/modules/messages/lib/searchLookup.js +8 -16
  47. package/dist/modules/messages/lib/searchLookup.js.map +2 -2
  48. package/dist/modules/query_index/api/openapi.js +13 -1
  49. package/dist/modules/query_index/api/openapi.js.map +2 -2
  50. package/dist/modules/query_index/api/status.js +165 -105
  51. package/dist/modules/query_index/api/status.js.map +2 -2
  52. package/dist/modules/query_index/components/QueryIndexesTable.js +128 -41
  53. package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
  54. package/dist/modules/query_index/lib/batch.js +11 -4
  55. package/dist/modules/query_index/lib/batch.js.map +2 -2
  56. package/dist/modules/query_index/lib/document.js +12 -5
  57. package/dist/modules/query_index/lib/document.js.map +2 -2
  58. package/dist/modules/query_index/lib/engine.js +36 -4
  59. package/dist/modules/query_index/lib/engine.js.map +2 -2
  60. package/dist/modules/query_index/lib/indexer.js +1 -1
  61. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  62. package/dist/modules/query_index/lib/search-tokens.js +7 -4
  63. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  64. package/dist/modules/sales/api/documents/factory.js +14 -3
  65. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  66. package/dist/modules/sales/commands/documents.js +22 -1
  67. package/dist/modules/sales/commands/documents.js.map +2 -2
  68. package/dist/modules/sales/components/useSalesChannelsEnabled.js +2 -35
  69. package/dist/modules/sales/components/useSalesChannelsEnabled.js.map +2 -2
  70. package/dist/modules/sales/data/validators.js +25 -4
  71. package/dist/modules/sales/data/validators.js.map +2 -2
  72. package/package.json +7 -7
  73. package/src/modules/auth/api/admin/nav.ts +24 -4
  74. package/src/modules/auth/api/users/route.ts +8 -28
  75. package/src/modules/auth/lib/backendChrome.tsx +51 -13
  76. package/src/modules/customers/api/utils.ts +12 -31
  77. package/src/modules/customers/cli.ts +11 -4
  78. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +4 -0
  79. package/src/modules/customers/components/formConfig.tsx +12 -8
  80. package/src/modules/dashboards/api/widgets/data/batch/route.ts +4 -0
  81. package/src/modules/dashboards/api/widgets/data/route.ts +21 -0
  82. package/src/modules/dashboards/lib/aggregations.ts +122 -43
  83. package/src/modules/dashboards/lib/exactDecimal.ts +96 -0
  84. package/src/modules/dashboards/services/widgetDataService.ts +342 -4
  85. package/src/modules/feature_toggles/components/FeatureGuard.tsx +9 -2
  86. package/src/modules/feature_toggles/components/hooks/staleTime.ts +5 -0
  87. package/src/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.ts +13 -1
  88. package/src/modules/feature_toggles/components/hooks/useFeatureFlagJson.ts +12 -3
  89. package/src/modules/feature_toggles/components/hooks/useFeatureFlagNumber.ts +10 -1
  90. package/src/modules/feature_toggles/components/hooks/useFeatureFlagString.ts +10 -1
  91. package/src/modules/inbox_ops/api/proposals/route.ts +20 -1
  92. package/src/modules/inbox_ops/subscribers/search-reindex-proposal-created.ts +39 -0
  93. package/src/modules/integrations/widgets/injection/external-ids/widget.client.tsx +0 -5
  94. package/src/modules/integrations/widgets/injection/external-ids/widget.ts +13 -0
  95. package/src/modules/messages/lib/searchLookup.ts +11 -34
  96. package/src/modules/query_index/api/openapi.ts +12 -0
  97. package/src/modules/query_index/api/status.ts +238 -136
  98. package/src/modules/query_index/components/QueryIndexesTable.tsx +192 -46
  99. package/src/modules/query_index/i18n/de.json +5 -3
  100. package/src/modules/query_index/i18n/en.json +5 -3
  101. package/src/modules/query_index/i18n/es.json +5 -3
  102. package/src/modules/query_index/i18n/pl.json +5 -3
  103. package/src/modules/query_index/lib/batch.ts +11 -4
  104. package/src/modules/query_index/lib/document.ts +35 -4
  105. package/src/modules/query_index/lib/engine.ts +53 -5
  106. package/src/modules/query_index/lib/indexer.ts +1 -1
  107. package/src/modules/query_index/lib/search-tokens.ts +13 -4
  108. package/src/modules/sales/api/documents/factory.ts +28 -3
  109. package/src/modules/sales/commands/documents.ts +26 -2
  110. package/src/modules/sales/components/useSalesChannelsEnabled.ts +2 -48
  111. package/src/modules/sales/data/validators.ts +50 -5
@@ -22,6 +22,7 @@ import {
22
22
  import { resolveSearchConfig, type SearchConfig } from '@open-mercato/shared/lib/search/config'
23
23
  import { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'
24
24
  import { runBeforeQueryPipeline, runAfterQueryPipeline, type QueryExtensionContext } from '@open-mercato/shared/lib/query/query-extension-runner'
25
+ import { warnOnCiphertextLikeFallback } from '@open-mercato/shared/lib/query/ciphertext-search-warning'
25
26
  import { resolveEncryptedSortFields, resolveEncryptedSortMaxRows, sortRowsInMemory } from '@open-mercato/shared/lib/query/encrypted-sort'
26
27
  import { mapWithConcurrency } from '@open-mercato/shared/lib/query/bounded-decrypt'
27
28
  import { createLogger } from '@open-mercato/shared/lib/logger'
@@ -415,12 +416,16 @@ export class HybridQueryEngine implements QueryEngine {
415
416
  const searchSources: SearchTokenSource[] = indexSources
416
417
  .map((src) => ({ entity: String(src.entityId), recordIdColumn: src.recordIdColumn }))
417
418
  .filter((src) => src.recordIdColumn && src.entity)
418
- const hasSearchTokens = searchEnabled && searchSources.length
419
+ const searchFilters = normalizedFilters.filter((filter) => filter.op === 'like' || filter.op === 'ilike')
420
+ // Probe `search_tokens` only when this query actually searches. Every consumer of
421
+ // `searchRuntime.enabled` already sits behind a like/ilike guard, so on a plain list load the
422
+ // answer is unused — and the probe is a `LIMIT 1` the planner can resolve as a seq scan over a
423
+ // large `search_tokens`. The join path below already probes lazily for the same reason.
424
+ const hasSearchTokens = searchEnabled && searchSources.length && searchFilters.length
419
425
  ? await this.searchSourcesHaveTokens(searchSources, opts.tenantId ?? null, orgScope)
420
426
  : false
421
427
  const searchRuntime: SearchRuntime = { ...searchRuntimeBase, searchSources, enabled: searchEnabled && hasSearchTokens }
422
428
  const joinSearchAvailability = new Map<string, boolean>()
423
- const searchFilters = normalizeFilters(opts.filters).filter((filter) => filter.op === 'like' || filter.op === 'ilike')
424
429
  if (searchFilters.length) {
425
430
  this.logSearchDebug('search:init', {
426
431
  entity,
@@ -446,6 +451,47 @@ export class HybridQueryEngine implements QueryEngine {
446
451
  organizationScope: orgScope,
447
452
  searchSources,
448
453
  })
454
+ const baseSearchFilters = [...baseFilters, ...cfFilters]
455
+ .filter((filter) => filter.op === 'like' || filter.op === 'ilike')
456
+ const fallbackFields = baseSearchFilters
457
+ .filter((filter) => !searchRuntime.enabled || typeof filter.value !== 'string' || tokenizeText(filter.value, searchConfig).hashes.length === 0)
458
+ .map((filter) => String(filter.field))
459
+ if (fallbackFields.length) {
460
+ await warnOnCiphertextLikeFallback({
461
+ entity: String(entity),
462
+ fields: fallbackFields,
463
+ tenantId: opts.tenantId ?? null,
464
+ // `searchEnabled` also folds in the missing-table case, which is
465
+ // "no usable tokens" rather than "the operator switched search off".
466
+ reason: searchRuntime.enabled
467
+ ? 'no-indexable-tokens'
468
+ : searchConfig.enabled ? 'no-search-tokens' : 'search-disabled',
469
+ service: this.getEncryptionService(),
470
+ })
471
+ }
472
+ }
473
+ for (const [alias, joinedFilters] of joinFilters) {
474
+ const filters = joinedFilters.filter((entry) => entry.op === 'like' || entry.op === 'ilike')
475
+ if (!filters.length) continue
476
+ const join = joinMap.get(alias)
477
+ if (!join?.entityId) continue
478
+ const hasJoinedTokens = searchEnabled
479
+ ? await this.hasSearchTokens(String(join.entityId), opts.tenantId ?? null, orgScope)
480
+ : false
481
+ joinSearchAvailability.set(String(join.entityId), hasJoinedTokens)
482
+ const fallbackFields = filters
483
+ .filter((filter) => !hasJoinedTokens || typeof filter.value !== 'string' || tokenizeText(filter.value, searchConfig).hashes.length === 0)
484
+ .map((filter) => filter.column)
485
+ if (!fallbackFields.length) continue
486
+ await warnOnCiphertextLikeFallback({
487
+ entity: String(join.entityId),
488
+ fields: fallbackFields,
489
+ tenantId: opts.tenantId ?? null,
490
+ reason: hasJoinedTokens
491
+ ? 'no-indexable-tokens'
492
+ : searchConfig.enabled ? 'no-search-tokens' : 'search-disabled',
493
+ service: this.getEncryptionService(),
494
+ })
449
495
  }
450
496
  const hasNonBaseSearchSource = searchSources.some(
451
497
  (src) => src.entity !== String(entity) || src.recordIdColumn !== 'b.id'
@@ -1636,9 +1682,13 @@ export class HybridQueryEngine implements QueryEngine {
1636
1682
  const orgScope = this.resolveOrganizationScope(opts)
1637
1683
  if (!opts.tenantId) throw new Error('QueryEngine: tenantId is required')
1638
1684
 
1685
+ const normalizedFilters = normalizeFilters(opts.filters)
1686
+ const hasSearchFilter = normalizedFilters.some((filter) => filter.op === 'like' || filter.op === 'ilike')
1687
+
1639
1688
  const searchConfig = resolveSearchConfig()
1640
1689
  const searchEnabled = searchConfig.enabled && await this.tableExists('search_tokens')
1641
- const hasSearchTokens = searchEnabled
1690
+ // Same gate as `query()`: without a like/ilike filter the probe's answer is never read.
1691
+ const hasSearchTokens = searchEnabled && hasSearchFilter
1642
1692
  ? await this.hasSearchTokens(entity, opts.tenantId ?? null, orgScope)
1643
1693
  : false
1644
1694
  const searchRuntime: SearchRuntime = {
@@ -1649,8 +1699,6 @@ export class HybridQueryEngine implements QueryEngine {
1649
1699
  mintAlias: createSearchAliasMinter(),
1650
1700
  }
1651
1701
 
1652
- const normalizedFilters = normalizeFilters(opts.filters)
1653
-
1654
1702
  const applyScope = (q: AnyBuilder): AnyBuilder => {
1655
1703
  let next = q
1656
1704
  .where(`${alias}.entity_type`, '=', entity)
@@ -127,7 +127,7 @@ export async function buildIndexDoc(em: EntityManager, params: BuildDocParams):
127
127
  // Kept outside the guard below: a failure while building the aggregate search field is a
128
128
  // bug in the aggregation or its configuration, and must surface instead of being mistaken
129
129
  // for an encryption failure and silently skipping encryption.
130
- doc = attachAggregateSearchField(doc)
130
+ doc = attachAggregateSearchField(doc, { entityType: params.entityType })
131
131
 
132
132
  try {
133
133
  const encryption = resolveTenantEncryptionService(em as any)
@@ -1,5 +1,9 @@
1
1
  import { type Kysely, sql } from 'kysely'
2
- import { resolveSearchConfig, type SearchConfig } from '@open-mercato/shared/lib/search/config'
2
+ import {
3
+ isSearchFieldBlocklisted,
4
+ resolveSearchConfig,
5
+ type SearchConfig,
6
+ } from '@open-mercato/shared/lib/search/config'
3
7
  import { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'
4
8
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
5
9
  import { createLogger } from '@open-mercato/shared/lib/logger'
@@ -62,13 +66,18 @@ function collectTextValues(value: unknown): string[] {
62
66
  return []
63
67
  }
64
68
 
65
- function shouldIndexField(field: string, value: unknown, config: SearchConfig): boolean {
69
+ function shouldIndexField(
70
+ field: string,
71
+ value: unknown,
72
+ config: SearchConfig,
73
+ entityType: string | null,
74
+ ): boolean {
66
75
  if (typeof value !== 'string' && !Array.isArray(value)) return false
67
76
  const lower = field.toLowerCase()
68
77
  if (lower === 'id' || lower.endsWith('_id') || lower.endsWith('.id')) return false
69
78
  if (lower.endsWith('_at')) return false
70
79
  if (['created_at', 'updated_at', 'deleted_at', 'tenant_id', 'organization_id'].includes(lower)) return false
71
- if (config.blocklistedFields.some((blocked) => lower.includes(blocked))) return false
80
+ if (isSearchFieldBlocklisted(field, entityType, config)) return false
72
81
  const values = collectTextValues(value)
73
82
  if (!values.length) return false
74
83
  return values.some((text) => tokenizeText(text, config).tokens.length > 0)
@@ -87,7 +96,7 @@ export function buildSearchTokenRows(params: BuildTokenOptions): SearchTokenRow[
87
96
  }
88
97
 
89
98
  for (const [field, rawValue] of Object.entries(params.doc)) {
90
- if (!shouldIndexField(field, rawValue, config)) continue
99
+ if (!shouldIndexField(field, rawValue, config, params.entityType)) continue
91
100
  const values = collectTextValues(rawValue)
92
101
  const seen = new Set<string>()
93
102
  for (const text of values) {
@@ -7,8 +7,11 @@ import { E } from '#generated/entities.ids.generated'
7
7
  import type { SalesOrder, SalesQuote } from '../../data/entities'
8
8
  import { SalesDocumentTagAssignment } from '../../data/entities'
9
9
  import {
10
+ ORDER_PAYMENT_LEDGER_FIELDS,
11
+ ORDER_PAYMENT_LEDGER_WARNING_CODE,
10
12
  orderCreateSchema,
11
13
  quoteCreateSchema,
14
+ type OrderPaymentLedgerWarning,
12
15
  } from '../../data/validators'
13
16
  import {
14
17
  createPagedListResponseSchema,
@@ -37,6 +40,13 @@ type DocumentBinding = {
37
40
  viewFeature: string
38
41
  }
39
42
 
43
+ type DocumentCreateResult = {
44
+ orderId?: string
45
+ quoteId?: string
46
+ id?: string
47
+ warnings?: OrderPaymentLedgerWarning[]
48
+ }
49
+
40
50
  const rawBodySchema = z.object({}).passthrough()
41
51
 
42
52
  const normalizeJsonRecord = (value: unknown): Record<string, unknown> | null => {
@@ -472,7 +482,12 @@ export function buildDocumentCrudOptions(binding: DocumentBinding) {
472
482
  )
473
483
  return parsed
474
484
  },
475
- response: ({ result }: { result: any }) => ({ id: result?.orderId ?? result?.quoteId ?? result?.id ?? null }),
485
+ response: ({ result }: { result?: DocumentCreateResult | null }) => ({
486
+ id: result?.orderId ?? result?.quoteId ?? result?.id ?? null,
487
+ ...(binding.kind === 'order' && Array.isArray(result?.warnings)
488
+ ? { warnings: result.warnings }
489
+ : {}),
490
+ }),
476
491
  status: 201,
477
492
  },
478
493
  update: {
@@ -616,8 +631,18 @@ export function buildDocumentOpenApi(binding: DocumentBinding) {
616
631
  listResponseSchema,
617
632
  create: {
618
633
  schema: createSchema,
619
- responseSchema: z.object({ id: z.string().uuid().nullable() }),
620
- description: `Creates a new sales ${binding.kind}.`,
634
+ responseSchema: binding.kind === 'order'
635
+ ? z.object({
636
+ id: z.string().uuid().nullable(),
637
+ warnings: z.array(z.object({
638
+ code: z.literal(ORDER_PAYMENT_LEDGER_WARNING_CODE),
639
+ fields: z.array(z.enum(ORDER_PAYMENT_LEDGER_FIELDS)),
640
+ })).optional(),
641
+ })
642
+ : z.object({ id: z.string().uuid().nullable() }),
643
+ description: binding.kind === 'order'
644
+ ? 'Creates a new sales order. paidTotalAmount, refundedTotalAmount, and outstandingAmount are deprecated compatibility inputs: supplied values are ignored and reported in warnings. Record payments through sales.payments.create or POST /api/sales/payments.'
645
+ : 'Creates a new sales quote.',
621
646
  },
622
647
  del: {
623
648
  schema: defaultDeleteRequestSchema,
@@ -76,6 +76,8 @@ import {
76
76
  quoteLineCreateSchema,
77
77
  quoteAdjustmentCreateSchema,
78
78
  orderCreateSchema,
79
+ ORDER_PAYMENT_LEDGER_WARNING_CODE,
80
+ resolveSuppliedOrderPaymentLedgerFields,
79
81
  orderLineCreateSchema,
80
82
  orderAdjustmentCreateSchema,
81
83
  invoiceCreateSchema,
@@ -87,6 +89,7 @@ import {
87
89
  type QuoteLineCreateInput,
88
90
  type QuoteAdjustmentCreateInput,
89
91
  type OrderCreateInput,
92
+ type OrderPaymentLedgerWarning,
90
93
  type OrderLineCreateInput,
91
94
  type OrderAdjustmentCreateInput,
92
95
  type InvoiceCreateInput,
@@ -143,6 +146,7 @@ import type { TranslateWithFallbackFn } from "@open-mercato/shared/lib/i18n/tran
143
146
  import { createLogger } from '@open-mercato/shared/lib/logger'
144
147
 
145
148
  const logger = createLogger('sales')
149
+ let warnedDeprecatedOrderPaymentLedgerInput = false
146
150
 
147
151
  // CRUD events configuration for workflow triggers
148
152
  const orderCrudEvents: CrudEventsConfig<SalesOrder> = {
@@ -5505,14 +5509,22 @@ const updateOrderCommand: CommandHandler<
5505
5509
 
5506
5510
  const createOrderCommand: CommandHandler<
5507
5511
  OrderCreateInput,
5508
- { orderId: string }
5512
+ { orderId: string; warnings?: OrderPaymentLedgerWarning[] }
5509
5513
  > = {
5510
5514
  id: "sales.orders.create",
5511
5515
  async execute(rawInput, ctx) {
5512
5516
  const generator = ctx.container.resolve(
5513
5517
  "salesDocumentNumberGenerator",
5514
5518
  ) as SalesDocumentNumberGenerator;
5519
+ const deprecatedPaymentLedgerFields = resolveSuppliedOrderPaymentLedgerFields(rawInput)
5515
5520
  const initial = orderCreateSchema.parse(rawInput ?? {});
5521
+ if (deprecatedPaymentLedgerFields.length && !warnedDeprecatedOrderPaymentLedgerInput) {
5522
+ warnedDeprecatedOrderPaymentLedgerInput = true
5523
+ logger.warn("sales.orders.create ignored deprecated payment ledger input", {
5524
+ code: ORDER_PAYMENT_LEDGER_WARNING_CODE,
5525
+ fields: deprecatedPaymentLedgerFields,
5526
+ })
5527
+ }
5516
5528
  const orderNumber =
5517
5529
  typeof initial.orderNumber === "string" &&
5518
5530
  initial.orderNumber.trim().length
@@ -5871,7 +5883,19 @@ const createOrderCommand: CommandHandler<
5871
5883
  previousStatus: null,
5872
5884
  });
5873
5885
 
5874
- return { orderId: order.id };
5886
+ return {
5887
+ orderId: order.id,
5888
+ ...(deprecatedPaymentLedgerFields.length
5889
+ ? {
5890
+ warnings: [
5891
+ {
5892
+ code: ORDER_PAYMENT_LEDGER_WARNING_CODE,
5893
+ fields: deprecatedPaymentLedgerFields,
5894
+ },
5895
+ ],
5896
+ }
5897
+ : {}),
5898
+ };
5875
5899
  },
5876
5900
  captureAfter: async (_input, result, ctx) => {
5877
5901
  const em = (ctx.container.resolve("em") as EntityManager).fork();
@@ -1,59 +1,13 @@
1
1
  "use client"
2
2
 
3
- import * as React from 'react'
4
- import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
3
+ import { useFeatureFlagBoolean } from '@open-mercato/core/modules/feature_toggles/components/hooks/useFeatureFlagBoolean'
5
4
  import { SALES_CHANNELS_TOGGLE_ID } from '../lib/salesChannelsToggleId'
6
5
 
7
6
  export { SALES_CHANNELS_TOGGLE_ID }
8
7
 
9
- const CACHE_TTL_MS = 60_000
10
-
11
- type FeatureToggleCheckResponse = {
12
- ok?: boolean
13
- value?: unknown
14
- }
15
-
16
8
  // Fails open: channels stay visible unless the toggle explicitly resolves to
17
9
  // false. A missing toggle definition (404) or an absent feature_toggles module
18
10
  // must not hide sales channel UI.
19
- async function fetchSalesChannelsEnabled(): Promise<boolean> {
20
- try {
21
- const call = await apiCall<FeatureToggleCheckResponse>(
22
- `/api/feature_toggles/check/boolean?identifier=${SALES_CHANNELS_TOGGLE_ID}`,
23
- )
24
- if (!call.ok) return true
25
- return !(call.result?.ok === true && call.result.value === false)
26
- } catch {
27
- return true
28
- }
29
- }
30
-
31
- let cache: { value: Promise<boolean>; at: number } | null = null
32
-
33
- function getSalesChannelsEnabled(): Promise<boolean> {
34
- const now = Date.now()
35
- if (!cache || now - cache.at > CACHE_TTL_MS) {
36
- cache = { value: fetchSalesChannelsEnabled(), at: now }
37
- }
38
- return cache.value
39
- }
40
-
41
11
  export function useSalesChannelsEnabled(): { enabled: boolean; isLoading: boolean } {
42
- const [resolved, setResolved] = React.useState<boolean | null>(null)
43
-
44
- React.useEffect(() => {
45
- let active = true
46
- getSalesChannelsEnabled()
47
- .then((value) => {
48
- if (active) setResolved(value)
49
- })
50
- .catch(() => {
51
- if (active) setResolved(true)
52
- })
53
- return () => {
54
- active = false
55
- }
56
- }, [])
57
-
58
- return { enabled: resolved !== false, isLoading: resolved === null }
12
+ return useFeatureFlagBoolean({ id: SALES_CHANNELS_TOGGLE_ID, defaultValue: true })
59
13
  }
@@ -624,6 +624,38 @@ export const quoteAdjustmentUpdateSchema = z
624
624
  })
625
625
  .merge(quoteAdjustmentCreateSchema.partial())
626
626
 
627
+ export const ORDER_PAYMENT_LEDGER_FIELDS = [
628
+ 'paidTotalAmount',
629
+ 'refundedTotalAmount',
630
+ 'outstandingAmount',
631
+ ] as const
632
+
633
+ export type OrderPaymentLedgerField = (typeof ORDER_PAYMENT_LEDGER_FIELDS)[number]
634
+
635
+ export const ORDER_PAYMENT_LEDGER_WARNING_CODE =
636
+ 'sales.order.payment_ledger_input_deprecated' as const
637
+
638
+ export type OrderPaymentLedgerWarning = {
639
+ code: typeof ORDER_PAYMENT_LEDGER_WARNING_CODE
640
+ fields: OrderPaymentLedgerField[]
641
+ }
642
+
643
+ export function resolveSuppliedOrderPaymentLedgerFields(value: unknown): OrderPaymentLedgerField[] {
644
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return []
645
+ return ORDER_PAYMENT_LEDGER_FIELDS.filter((field) =>
646
+ Object.prototype.hasOwnProperty.call(value, field),
647
+ )
648
+ }
649
+
650
+ const orderPaymentLedgerShape = {
651
+ /** @deprecated Derived from recorded payments. Use sales.payments.create or POST /api/sales/payments. */
652
+ paidTotalAmount: decimal({ min: 0 }).optional(),
653
+ /** @deprecated Derived from recorded payments. Use sales.payments.create or POST /api/sales/payments. */
654
+ refundedTotalAmount: decimal({ min: 0 }).optional(),
655
+ /** @deprecated Derived from recorded payments. Use sales.payments.create or POST /api/sales/payments. */
656
+ outstandingAmount: decimal().optional(),
657
+ }
658
+
627
659
  const orderTotalsSchema = z.object({
628
660
  subtotalNetAmount: decimal({ min: 0 }).optional(),
629
661
  subtotalGrossAmount: decimal({ min: 0 }).optional(),
@@ -634,9 +666,6 @@ const orderTotalsSchema = z.object({
634
666
  surchargeTotalAmount: decimal({ min: 0 }).optional(),
635
667
  grandTotalNetAmount: decimal({ min: 0 }).optional(),
636
668
  grandTotalGrossAmount: decimal({ min: 0 }).optional(),
637
- paidTotalAmount: decimal({ min: 0 }).optional(),
638
- refundedTotalAmount: decimal({ min: 0 }).optional(),
639
- outstandingAmount: decimal().optional(),
640
669
  lineItemCount: z.coerce.number().int().min(0).optional(),
641
670
  })
642
671
 
@@ -690,6 +719,7 @@ export const orderCreateSchema = scoped.extend({
690
719
  adjustments: z.array(orderAdjustmentCreateSchema.omit({ organizationId: true, tenantId: true, orderId: true })).optional(),
691
720
  tags: z.array(uuid()).optional(),
692
721
  ...orderTotalsSchema.shape,
722
+ ...orderPaymentLedgerShape,
693
723
  })
694
724
 
695
725
  export const orderUpdateSchema = z
@@ -1046,8 +1076,23 @@ export type PaymentMethodCreateInput = z.infer<typeof paymentMethodCreateSchema>
1046
1076
  export type PaymentMethodUpdateInput = z.infer<typeof paymentMethodUpdateSchema>
1047
1077
  export type TaxRateCreateInput = z.infer<typeof taxRateCreateSchema>
1048
1078
  export type TaxRateUpdateInput = z.infer<typeof taxRateUpdateSchema>
1049
- export type OrderCreateInput = z.infer<typeof orderCreateSchema>
1050
- export type OrderUpdateInput = z.infer<typeof orderUpdateSchema>
1079
+ export type DeprecatedOrderPaymentLedgerInput = {
1080
+ /** @deprecated Derived from recorded payments. Use sales.payments.create or POST /api/sales/payments. */
1081
+ paidTotalAmount?: number
1082
+ /** @deprecated Derived from recorded payments. Use sales.payments.create or POST /api/sales/payments. */
1083
+ refundedTotalAmount?: number
1084
+ /** @deprecated Derived from recorded payments. Use sales.payments.create or POST /api/sales/payments. */
1085
+ outstandingAmount?: number
1086
+ }
1087
+
1088
+ export type OrderCreateInput = Omit<
1089
+ z.infer<typeof orderCreateSchema>,
1090
+ OrderPaymentLedgerField
1091
+ > & DeprecatedOrderPaymentLedgerInput
1092
+ export type OrderUpdateInput = Omit<
1093
+ z.infer<typeof orderUpdateSchema>,
1094
+ OrderPaymentLedgerField
1095
+ > & DeprecatedOrderPaymentLedgerInput
1051
1096
  export type OrderLineCreateInput = z.infer<typeof orderLineCreateSchema>
1052
1097
  export type OrderLineUpdateInput = z.infer<typeof orderLineUpdateSchema>
1053
1098
  export type OrderAdjustmentCreateInput = z.infer<typeof orderAdjustmentCreateSchema>