@open-mercato/core 0.7.1-develop.7137.1.26575786c0 → 0.7.1-develop.7148.1.3076e5ccf7
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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/backend/users/page.js +4 -4
- package/dist/modules/auth/backend/users/page.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js +17 -4
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
- package/dist/modules/customers/components/linking/adapters/personAdapter.js +1 -1
- package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260901120000_reindex_pipeline_stage_colors.js +16 -0
- package/dist/modules/customers/migrations/Migration20260901120000_reindex_pipeline_stage_colors.js.map +7 -0
- package/dist/modules/dictionaries/migrations/Migration20260901120000_reindex_dictionary_entries.js +16 -0
- package/dist/modules/dictionaries/migrations/Migration20260901120000_reindex_dictionary_entries.js.map +7 -0
- package/dist/modules/integrations/backend/integrations/[id]/page.js +16 -3
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/useIntegrationCredentialsFeatureAccess.js +42 -0
- package/dist/modules/integrations/backend/integrations/useIntegrationCredentialsFeatureAccess.js.map +7 -0
- package/dist/modules/messages/api/route.js +2 -1
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +11 -1
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/lib/calculations.js +22 -0
- package/dist/modules/sales/lib/calculations.js.map +2 -2
- package/dist/modules/sales/lib/lineSnapshots.js +11 -1
- package/dist/modules/sales/lib/lineSnapshots.js.map +2 -2
- package/dist/modules/workflows/migrations/Migration20260901120000_reindex_workflow_definitions.js +16 -0
- package/dist/modules/workflows/migrations/Migration20260901120000_reindex_workflow_definitions.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/auth/backend/users/page.tsx +4 -4
- package/src/modules/auth/i18n/de.json +4 -0
- package/src/modules/auth/i18n/en.json +4 -0
- package/src/modules/auth/i18n/es.json +4 -0
- package/src/modules/auth/i18n/ko.json +4 -0
- package/src/modules/auth/i18n/pl.json +4 -0
- package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +17 -4
- package/src/modules/customers/components/linking/adapters/personAdapter.tsx +2 -1
- package/src/modules/customers/i18n/de.json +5 -0
- package/src/modules/customers/i18n/en.json +5 -0
- package/src/modules/customers/i18n/es.json +5 -0
- package/src/modules/customers/i18n/ko.json +5 -0
- package/src/modules/customers/i18n/pl.json +5 -0
- package/src/modules/customers/migrations/Migration20260901120000_reindex_pipeline_stage_colors.ts +22 -0
- package/src/modules/dictionaries/migrations/Migration20260901120000_reindex_dictionary_entries.ts +20 -0
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +23 -2
- package/src/modules/integrations/backend/integrations/useIntegrationCredentialsFeatureAccess.ts +50 -0
- package/src/modules/integrations/i18n/de.json +1 -0
- package/src/modules/integrations/i18n/en.json +1 -0
- package/src/modules/integrations/i18n/es.json +1 -0
- package/src/modules/integrations/i18n/ko.json +1 -0
- package/src/modules/integrations/i18n/pl.json +1 -0
- package/src/modules/messages/api/route.ts +2 -1
- package/src/modules/sales/commands/documents.ts +14 -0
- package/src/modules/sales/lib/calculations.ts +41 -0
- package/src/modules/sales/lib/lineSnapshots.ts +23 -0
- package/src/modules/sales/lib/types.ts +9 -0
- package/src/modules/workflows/migrations/Migration20260901120000_reindex_workflow_definitions.ts +21 -0
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
type SecretFieldsConfigured,
|
|
59
59
|
} from '../credential-secret-fields'
|
|
60
60
|
import { isValidCredentialUrl } from '../../../lib/credentials-field-validation'
|
|
61
|
+
import { useIntegrationCredentialsFeatureAccess } from '../useIntegrationCredentialsFeatureAccess'
|
|
61
62
|
|
|
62
63
|
type CredentialField = IntegrationCredentialField
|
|
63
64
|
type BuiltInIntegrationDetailTab = 'credentials' | 'version' | 'health' | 'logs' | 'data-sync-schedule'
|
|
@@ -65,6 +66,11 @@ type IntegrationDetailTab = BuiltInIntegrationDetailTab | string
|
|
|
65
66
|
|
|
66
67
|
const UNSUPPORTED_CREDENTIAL_FIELD_TYPES = new Set<CredentialFieldType>(['oauth', 'ssh_keypair'])
|
|
67
68
|
|
|
69
|
+
// `/api/integrations/{id}/credentials` requires `integrations.credentials.manage`, so a viewer
|
|
70
|
+
// without the grant gets an expected 403 that the permission notice already explains. Opting out
|
|
71
|
+
// of the global forbidden handling keeps it from raising an "Access denied" flash and throwing.
|
|
72
|
+
const credentialsRequestHeaders = { 'x-om-forbidden-redirect': '0' } as const
|
|
73
|
+
|
|
68
74
|
function isEditableCredentialField(field: CredentialField): boolean {
|
|
69
75
|
return !UNSUPPORTED_CREDENTIAL_FIELD_TYPES.has(field.type)
|
|
70
76
|
}
|
|
@@ -463,6 +469,10 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
463
469
|
const [activeTab, setActiveTab] = React.useState<IntegrationDetailTab>('credentials')
|
|
464
470
|
|
|
465
471
|
const credentialsFormId = React.useId()
|
|
472
|
+
const {
|
|
473
|
+
isLoading: isLoadingCredentialsAccess,
|
|
474
|
+
canManageCredentials,
|
|
475
|
+
} = useIntegrationCredentialsFeatureAccess()
|
|
466
476
|
|
|
467
477
|
const resolveCurrentIntegrationId = React.useCallback(() => {
|
|
468
478
|
return integrationId ?? (
|
|
@@ -516,7 +526,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
516
526
|
updatedAt?: string | null
|
|
517
527
|
}>(
|
|
518
528
|
`/api/integrations/${encodeURIComponent(currentIntegrationId)}/credentials`,
|
|
519
|
-
|
|
529
|
+
{ headers: credentialsRequestHeaders },
|
|
520
530
|
{ fallback: null },
|
|
521
531
|
)
|
|
522
532
|
if (call.ok && call.result) {
|
|
@@ -1032,7 +1042,10 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
1032
1042
|
? 'border-status-success-border bg-status-success-bg text-status-success-text'
|
|
1033
1043
|
: 'border-status-neutral-border bg-status-neutral-bg text-status-neutral-text'
|
|
1034
1044
|
|
|
1035
|
-
const showCredentialActions = showCredentialsTab
|
|
1045
|
+
const showCredentialActions = showCredentialsTab
|
|
1046
|
+
&& activeTab === 'credentials'
|
|
1047
|
+
&& credentialFormFields.length > 0
|
|
1048
|
+
&& canManageCredentials
|
|
1036
1049
|
|
|
1037
1050
|
React.useEffect(() => {
|
|
1038
1051
|
setActiveTab(resolveRequestedIntegrationDetailTab(searchParams?.get('tab'), visibleTabIds))
|
|
@@ -1280,6 +1293,14 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
|
|
|
1280
1293
|
<p className="text-sm text-muted-foreground">
|
|
1281
1294
|
{t('integrations.detail.credentials.notConfigured')}
|
|
1282
1295
|
</p>
|
|
1296
|
+
) : isLoadingCredentialsAccess ? (
|
|
1297
|
+
<div className="flex justify-center py-8"><Spinner /></div>
|
|
1298
|
+
) : !canManageCredentials ? (
|
|
1299
|
+
<EmptyState
|
|
1300
|
+
size="sm"
|
|
1301
|
+
icon={<Key className="h-8 w-8" aria-hidden="true" />}
|
|
1302
|
+
title={t('integrations.detail.credentials.noPermission', 'You do not have permission to manage credentials for this integration.')}
|
|
1303
|
+
/>
|
|
1283
1304
|
) : (
|
|
1284
1305
|
<CrudForm<Record<string, unknown>>
|
|
1285
1306
|
key={`${resolvedIntegration.id}:${credentialsFormKey}`}
|
package/src/modules/integrations/backend/integrations/useIntegrationCredentialsFeatureAccess.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
5
|
+
import { hasFeature } from '@open-mercato/shared/security/features'
|
|
6
|
+
|
|
7
|
+
type FeatureCheckResponse = {
|
|
8
|
+
granted?: string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useIntegrationCredentialsFeatureAccess() {
|
|
12
|
+
const [granted, setGranted] = React.useState<string[]>([])
|
|
13
|
+
const [isLoading, setIsLoading] = React.useState(true)
|
|
14
|
+
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
let cancelled = false
|
|
17
|
+
|
|
18
|
+
async function load() {
|
|
19
|
+
try {
|
|
20
|
+
const call = await apiCall<FeatureCheckResponse>('/api/auth/feature-check', {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: { 'content-type': 'application/json' },
|
|
23
|
+
body: JSON.stringify({
|
|
24
|
+
features: ['integrations.credentials.manage'],
|
|
25
|
+
}),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
if (!cancelled) {
|
|
29
|
+
setGranted(Array.isArray(call.result?.granted) ? call.result.granted : [])
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
if (!cancelled) setGranted([])
|
|
33
|
+
} finally {
|
|
34
|
+
if (!cancelled) setIsLoading(false)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
void load()
|
|
39
|
+
|
|
40
|
+
return () => {
|
|
41
|
+
cancelled = true
|
|
42
|
+
}
|
|
43
|
+
}, [])
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
isLoading,
|
|
47
|
+
granted,
|
|
48
|
+
canManageCredentials: hasFeature(granted, 'integrations.credentials.manage'),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"integrations.detail.back": "Zurück zu Integrationen",
|
|
12
12
|
"integrations.detail.backToList": "Zurück zu Integrationen",
|
|
13
13
|
"integrations.detail.credentials.bundleShared": "Gemeinsame Zugangsdaten aus Paket: {bundle}",
|
|
14
|
+
"integrations.detail.credentials.noPermission": "Sie haben keine Berechtigung, die Zugangsdaten für diese Integration zu verwalten.",
|
|
14
15
|
"integrations.detail.credentials.notConfigured": "Noch keine Zugangsdaten konfiguriert",
|
|
15
16
|
"integrations.detail.credentials.save": "Zugangsdaten speichern",
|
|
16
17
|
"integrations.detail.credentials.saveError": "Zugangsdaten konnten nicht gespeichert werden",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"integrations.detail.back": "Back to Integrations",
|
|
12
12
|
"integrations.detail.backToList": "Back to integrations",
|
|
13
13
|
"integrations.detail.credentials.bundleShared": "Shared credentials from bundle: {bundle}",
|
|
14
|
+
"integrations.detail.credentials.noPermission": "You do not have permission to manage credentials for this integration.",
|
|
14
15
|
"integrations.detail.credentials.notConfigured": "No credentials configured yet",
|
|
15
16
|
"integrations.detail.credentials.save": "Save Credentials",
|
|
16
17
|
"integrations.detail.credentials.saveError": "Failed to save credentials",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"integrations.detail.back": "Volver a integraciones",
|
|
12
12
|
"integrations.detail.backToList": "Volver a integraciones",
|
|
13
13
|
"integrations.detail.credentials.bundleShared": "Credenciales compartidas del paquete: {bundle}",
|
|
14
|
+
"integrations.detail.credentials.noPermission": "No tiene permiso para gestionar las credenciales de esta integración.",
|
|
14
15
|
"integrations.detail.credentials.notConfigured": "No hay credenciales configuradas",
|
|
15
16
|
"integrations.detail.credentials.save": "Guardar credenciales",
|
|
16
17
|
"integrations.detail.credentials.saveError": "No se pudieron guardar las credenciales",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"integrations.detail.back": "통합으로 돌아가기",
|
|
12
12
|
"integrations.detail.backToList": "통합 목록으로 돌아가기",
|
|
13
13
|
"integrations.detail.credentials.bundleShared": "번들에서 공유된 자격 증명: {bundle}",
|
|
14
|
+
"integrations.detail.credentials.noPermission": "이 통합의 자격 증명을 관리할 권한이 없습니다.",
|
|
14
15
|
"integrations.detail.credentials.notConfigured": "아직 구성된 자격 증명이 없습니다",
|
|
15
16
|
"integrations.detail.credentials.save": "자격 증명 저장",
|
|
16
17
|
"integrations.detail.credentials.saveError": "자격 증명 저장에 실패했습니다",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"integrations.detail.back": "Powrót do integracji",
|
|
12
12
|
"integrations.detail.backToList": "Wróć do integracji",
|
|
13
13
|
"integrations.detail.credentials.bundleShared": "Współdzielone dane z pakietu: {bundle}",
|
|
14
|
+
"integrations.detail.credentials.noPermission": "Nie masz uprawnień do zarządzania danymi uwierzytelniającymi tej integracji.",
|
|
14
15
|
"integrations.detail.credentials.notConfigured": "Brak skonfigurowanych danych",
|
|
15
16
|
"integrations.detail.credentials.save": "Zapisz dane",
|
|
16
17
|
"integrations.detail.credentials.saveError": "Nie udało się zapisać danych",
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
composeSourceHintSchema,
|
|
23
23
|
resolveComposeSourceChannelType,
|
|
24
24
|
} from '../lib/composeSourceChannelType'
|
|
25
|
+
import { resolveMessageActionData } from '../lib/actions'
|
|
25
26
|
import { MESSAGE_ATTACHMENT_ENTITY_ID } from '../lib/constants'
|
|
26
27
|
import { getMessageType } from '../lib/message-types-registry'
|
|
27
28
|
import { validateMessageObjectsForType } from '../lib/object-validation'
|
|
@@ -382,7 +383,7 @@ export async function GET(req: Request) {
|
|
|
382
383
|
if (!message) return null
|
|
383
384
|
const body = typeof message.body === 'string' ? message.body : ''
|
|
384
385
|
const bodyPreview = body.substring(0, 150) + (body.length > 150 ? '...' : '')
|
|
385
|
-
const actionData = message
|
|
386
|
+
const actionData = resolveMessageActionData(message)
|
|
386
387
|
return {
|
|
387
388
|
...(senderMetaById.get(row.sender_user_id)
|
|
388
389
|
? {
|
|
@@ -134,6 +134,7 @@ import {
|
|
|
134
134
|
mapOrderLineEntityToSnapshot,
|
|
135
135
|
mapQuoteLineEntityToSnapshot,
|
|
136
136
|
resolveUpsertDiscountFields,
|
|
137
|
+
resolveUpsertTotalsOrigin,
|
|
137
138
|
} from "../lib/lineSnapshots";
|
|
138
139
|
import { loadShippedQuantityByLine } from "../lib/shipments/snapshots";
|
|
139
140
|
import { resolveDictionaryEntryValue, resolveCachedDictionaryEntryValue } from "../lib/dictionaries";
|
|
@@ -3038,6 +3039,13 @@ function isStoredRowSourcedLine(line: DocumentLineCreateInput): boolean {
|
|
|
3038
3039
|
);
|
|
3039
3040
|
}
|
|
3040
3041
|
|
|
3042
|
+
function isStoredRowSourcedTotalsLine(line: DocumentLineCreateInput): boolean {
|
|
3043
|
+
return (
|
|
3044
|
+
(line as Pick<SalesLineSnapshot, "totalsFromStoredRow">)
|
|
3045
|
+
.totalsFromStoredRow === true
|
|
3046
|
+
);
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3041
3049
|
function createLineSnapshotFromInput(
|
|
3042
3050
|
line: DocumentLineCreateInput,
|
|
3043
3051
|
lineNumber: number,
|
|
@@ -3079,6 +3087,10 @@ function createLineSnapshotFromInput(
|
|
|
3079
3087
|
...(isStoredRowSourcedLine(line)
|
|
3080
3088
|
? { discountAmountFromStoredRow: true }
|
|
3081
3089
|
: { discountAmountBasis: line.discountAmountBasis ?? "unit" }),
|
|
3090
|
+
// Carried independently of the discount origin above: a line upsert can
|
|
3091
|
+
// take its discount from the caller while its totals still come off the
|
|
3092
|
+
// stored row, and only the caller half is worth reconciling (#5644).
|
|
3093
|
+
...(isStoredRowSourcedTotalsLine(line) ? { totalsFromStoredRow: true } : {}),
|
|
3082
3094
|
discountPercent: line.discountPercent ?? null,
|
|
3083
3095
|
taxRate: line.taxRate ?? null,
|
|
3084
3096
|
taxAmount: line.taxAmount ?? null,
|
|
@@ -7240,6 +7252,7 @@ const orderLineUpsertCommand: CommandHandler<
|
|
|
7240
7252
|
parsed.totalNetAmount ?? existingSnapshot?.totalNetAmount ?? null,
|
|
7241
7253
|
totalGrossAmount:
|
|
7242
7254
|
parsed.totalGrossAmount ?? existingSnapshot?.totalGrossAmount ?? null,
|
|
7255
|
+
...resolveUpsertTotalsOrigin(parsed.totalNetAmount, existingSnapshot),
|
|
7243
7256
|
configuration:
|
|
7244
7257
|
parsed.configuration ?? existingSnapshot?.configuration ?? null,
|
|
7245
7258
|
promotionCode:
|
|
@@ -7737,6 +7750,7 @@ const quoteLineUpsertCommand: CommandHandler<
|
|
|
7737
7750
|
parsed.totalNetAmount ?? existingSnapshot?.totalNetAmount ?? null,
|
|
7738
7751
|
totalGrossAmount:
|
|
7739
7752
|
parsed.totalGrossAmount ?? existingSnapshot?.totalGrossAmount ?? null,
|
|
7753
|
+
...resolveUpsertTotalsOrigin(parsed.totalNetAmount, existingSnapshot),
|
|
7740
7754
|
configuration:
|
|
7741
7755
|
parsed.configuration ?? existingSnapshot?.configuration ?? null,
|
|
7742
7756
|
promotionCode:
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
1
2
|
import {
|
|
2
3
|
type SalesAdjustmentDraft,
|
|
3
4
|
type SalesCalculationContext,
|
|
@@ -11,6 +12,8 @@ import {
|
|
|
11
12
|
type SalesTotalsCalculationHook,
|
|
12
13
|
} from './types'
|
|
13
14
|
|
|
15
|
+
const logger = createLogger('sales')
|
|
16
|
+
|
|
14
17
|
function toNumber(value: unknown, fallback = 0): number {
|
|
15
18
|
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
16
19
|
if (typeof value === 'string' && value.trim() !== '' && !Number.isNaN(Number(value))) {
|
|
@@ -23,6 +26,12 @@ function round(value: number): number {
|
|
|
23
26
|
return Math.round((value + Number.EPSILON) * 1e4) / 1e4
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
// The engine rounds to the 4 decimals the numeric columns carry, but callers
|
|
30
|
+
// work in money at 2, so an exact comparison would report half a cent of
|
|
31
|
+
// honest rounding as a mismatch. Half a minor unit is the widest divergence
|
|
32
|
+
// that cannot be a real discrepancy and the narrowest that silences that noise.
|
|
33
|
+
const NET_RECONCILIATION_TOLERANCE = 0.005
|
|
34
|
+
|
|
26
35
|
function extractAdjustmentTaxRate(adjustment: SalesAdjustmentDraft): number | null {
|
|
27
36
|
const metadata = (adjustment.metadata ?? {}) as Record<string, unknown>
|
|
28
37
|
const candidate =
|
|
@@ -120,6 +129,38 @@ function buildBaseLineResult(line: SalesLineSnapshot): SalesLineCalculationResul
|
|
|
120
129
|
netSubtotalBeforeDiscount,
|
|
121
130
|
)
|
|
122
131
|
const netSubtotal = Math.max(netSubtotalBeforeDiscount - discountTotal, 0)
|
|
132
|
+
// Unlike totalGrossAmount below, a supplied totalNetAmount is never honoured
|
|
133
|
+
// verbatim — net always comes from unitPriceNet/discount so it stays
|
|
134
|
+
// internally consistent with them. A caller-supplied value is still
|
|
135
|
+
// reconciled against the computed one so a divergence (e.g. a mis-read
|
|
136
|
+
// discount) surfaces instead of being silently discarded (#5644).
|
|
137
|
+
//
|
|
138
|
+
// Only a caller's value is reconciled: a snapshot rebuilt from a persisted
|
|
139
|
+
// row (`totalsFromStoredRow`) carries the engine's own previous output, and
|
|
140
|
+
// on a row the discount contract still has to heal that value is *supposed*
|
|
141
|
+
// to differ from the recomputed net. Warning about it would drown the caller
|
|
142
|
+
// signal this exists for in one line per line per recalculation.
|
|
143
|
+
if (line.totalsFromStoredRow !== true && line.totalNetAmount !== null && line.totalNetAmount !== undefined) {
|
|
144
|
+
const computedNetAmount = round(netSubtotal)
|
|
145
|
+
const suppliedNetAmount = toNumber(line.totalNetAmount, NaN)
|
|
146
|
+
if (!Number.isFinite(suppliedNetAmount)) {
|
|
147
|
+
// Falling back to the computed value here would compare equal and log
|
|
148
|
+
// nothing — the same silent discard #5644 exists to end.
|
|
149
|
+
logger.warn('Sales line totalNetAmount is not a finite number; the computed value is used', {
|
|
150
|
+
lineId: line.id ?? null,
|
|
151
|
+
productId: line.productId ?? null,
|
|
152
|
+
suppliedTotalNetAmount: line.totalNetAmount,
|
|
153
|
+
computedNetAmount,
|
|
154
|
+
})
|
|
155
|
+
} else if (Math.abs(round(suppliedNetAmount) - computedNetAmount) > NET_RECONCILIATION_TOLERANCE) {
|
|
156
|
+
logger.warn('Sales line totalNetAmount does not match the computed net amount; the computed value is used', {
|
|
157
|
+
lineId: line.id ?? null,
|
|
158
|
+
productId: line.productId ?? null,
|
|
159
|
+
suppliedTotalNetAmount: round(suppliedNetAmount),
|
|
160
|
+
computedNetAmount,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
}
|
|
123
164
|
const explicitTaxAmount = line.taxAmount !== null && line.taxAmount !== undefined
|
|
124
165
|
let taxAmount = explicitTaxAmount
|
|
125
166
|
? toNumber(line.taxAmount, 0)
|
|
@@ -39,6 +39,11 @@ function mapPersistedLine(line: SalesOrderLine | SalesQuoteLine): SalesLineSnaps
|
|
|
39
39
|
discountPercent: toNumeric(line.discountPercent),
|
|
40
40
|
taxRate: toNumeric(line.taxRate),
|
|
41
41
|
taxAmount: toNumeric(line.taxAmount),
|
|
42
|
+
// The totals below are the engine's own previous output read back off the
|
|
43
|
+
// row, not something a caller asserted, so they are not reconciled against
|
|
44
|
+
// the recomputed net (#5644) — on a legacy row that divergence is the
|
|
45
|
+
// discount contract healing itself, not a caller mistake.
|
|
46
|
+
totalsFromStoredRow: true,
|
|
42
47
|
totalNetAmount: toNumeric(line.totalNetAmount),
|
|
43
48
|
totalGrossAmount: toNumeric(line.totalGrossAmount),
|
|
44
49
|
configuration: line.configuration ? cloneJson(line.configuration) : null,
|
|
@@ -96,3 +101,21 @@ export function resolveUpsertDiscountFields(
|
|
|
96
101
|
discountAmountFromStoredRow: existingSnapshot != null,
|
|
97
102
|
}
|
|
98
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Decide whether the `totalNetAmount` an upsert payload ends up carrying is a
|
|
107
|
+
* caller assertion or a value that came back off the stored row.
|
|
108
|
+
*
|
|
109
|
+
* The upsert merges caller input over the existing snapshot, so the merged
|
|
110
|
+
* total has two possible origins and only the caller one is worth reconciling
|
|
111
|
+
* against the recomputed net (#5644): a value read back off the row is what the
|
|
112
|
+
* engine itself wrote last time, and on a legacy row it is exactly what
|
|
113
|
+
* recalculation is supposed to heal.
|
|
114
|
+
*/
|
|
115
|
+
export function resolveUpsertTotalsOrigin(
|
|
116
|
+
callerTotalNetAmount: number | null | undefined,
|
|
117
|
+
existingSnapshot: Pick<SalesLineSnapshot, 'totalNetAmount'> | null | undefined,
|
|
118
|
+
): Pick<SalesLineSnapshot, 'totalsFromStoredRow'> {
|
|
119
|
+
if (callerTotalNetAmount !== null && callerTotalNetAmount !== undefined) return {}
|
|
120
|
+
return existingSnapshot != null ? { totalsFromStoredRow: true } : {}
|
|
121
|
+
}
|
|
@@ -73,6 +73,15 @@ export type SalesLineSnapshot = {
|
|
|
73
73
|
discountPercent?: number | null
|
|
74
74
|
taxRate?: number | null
|
|
75
75
|
taxAmount?: number | null
|
|
76
|
+
/**
|
|
77
|
+
* Set by entity-to-snapshot mappers ONLY. Marks `totalNetAmount` /
|
|
78
|
+
* `totalGrossAmount` as reconstructed from a persisted row, so they are the
|
|
79
|
+
* engine's own previous output rather than a caller assertion. That is what
|
|
80
|
+
* keeps the #5644 reconciliation a *caller* signal: a stored net is expected
|
|
81
|
+
* to diverge on a row the discount contract heals on the next pass. Never
|
|
82
|
+
* persisted; never accepted from a request.
|
|
83
|
+
*/
|
|
84
|
+
totalsFromStoredRow?: boolean
|
|
76
85
|
totalNetAmount?: number | null
|
|
77
86
|
totalGrossAmount?: number | null
|
|
78
87
|
configuration?: Record<string, unknown> | null
|
package/src/modules/workflows/migrations/Migration20260901120000_reindex_workflow_definitions.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
import { declareQueryIndexReindex } from '@open-mercato/shared/lib/query/migration-reindex';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Catch-up reindex for `Migration20260428102318`, which renamed three seeded
|
|
6
|
+
* `workflow_definitions.workflow_id` values in raw SQL and never notified the query index. On
|
|
7
|
+
* every install that already applied it, `entity_indexes.doc` for `workflows:workflow_definition`
|
|
8
|
+
* still resolves those records by the pre-rename identifier — `checkout_simple_v1` rather than
|
|
9
|
+
* `workflows.checkout-demo`, and so on — which is exactly the identifier the rename removed.
|
|
10
|
+
*
|
|
11
|
+
* This migration executes no SQL — the declaration below is its entire payload.
|
|
12
|
+
*/
|
|
13
|
+
export const queryIndexReindexEntityTypes = declareQueryIndexReindex([
|
|
14
|
+
'workflows:workflow_definition',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export class Migration20260901120000_reindex_workflow_definitions extends Migration {
|
|
18
|
+
override up(): void | Promise<void> {}
|
|
19
|
+
|
|
20
|
+
override down(): void | Promise<void> {}
|
|
21
|
+
}
|