@open-mercato/core 0.7.1-develop.7136.1.0f76137a1d → 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/backend/customers/companies-v2/[id]/page.js +1 -0
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +1 -0
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +1 -0
- package/dist/modules/customers/backend/customers/people-v2/[id]/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/detail/ScheduleActivityDialog.js +25 -12
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.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/backend/customers/companies-v2/[id]/page.tsx +1 -0
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +1 -0
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +1 -0
- package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +17 -4
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +40 -14
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +2 -0
- package/src/modules/customers/components/linking/adapters/personAdapter.tsx +2 -1
- package/src/modules/customers/i18n/de.json +6 -1
- package/src/modules/customers/i18n/en.json +6 -1
- package/src/modules/customers/i18n/es.json +6 -1
- package/src/modules/customers/i18n/ko.json +6 -1
- package/src/modules/customers/i18n/pl.json +6 -1
- 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
|
@@ -1715,6 +1715,11 @@
|
|
|
1715
1715
|
"customers.linking.person.dialogSubtitle": "Einen bestehenden Kontakt mit diesem Unternehmen verknüpfen",
|
|
1716
1716
|
"customers.linking.person.dialogSubtitleFor": "Einen bestehenden Kontakt mit {{name}} verknüpfen",
|
|
1717
1717
|
"customers.linking.person.dialogTitle": "Personen verknüpfen",
|
|
1718
|
+
"customers.linking.person.role.all": "Alle",
|
|
1719
|
+
"customers.linking.person.role.budgetHolder": "Budgetverantwortlicher",
|
|
1720
|
+
"customers.linking.person.role.contact": "Kontaktperson",
|
|
1721
|
+
"customers.linking.person.role.decisionMaker": "Entscheider",
|
|
1722
|
+
"customers.linking.person.role.stakeholder": "Stakeholder",
|
|
1718
1723
|
"customers.linking.person.searchEmpty": "Keine passenden Personen gefunden.",
|
|
1719
1724
|
"customers.linking.person.searchPlaceholder": "Alle Personen durchsuchen…",
|
|
1720
1725
|
"customers.linking.person.sectionLabel": "PASSENDE KONTAKTE",
|
|
@@ -2658,7 +2663,7 @@
|
|
|
2658
2663
|
"customers.schedule.task.priority.high": "Hoch",
|
|
2659
2664
|
"customers.schedule.task.priority.low": "Niedrig",
|
|
2660
2665
|
"customers.schedule.task.priority.medium": "Mittel",
|
|
2661
|
-
"customers.schedule.task.priority.
|
|
2666
|
+
"customers.schedule.task.priority.none": "Keine",
|
|
2662
2667
|
"customers.schedule.task.priorityLabel": "Priorität",
|
|
2663
2668
|
"customers.schedule.task.save": "Aufgabe speichern",
|
|
2664
2669
|
"customers.schedule.task.subtitle": "Erfassen Sie etwas zum Nachverfolgen",
|
|
@@ -1715,6 +1715,11 @@
|
|
|
1715
1715
|
"customers.linking.person.dialogSubtitle": "Link an existing contact to this company",
|
|
1716
1716
|
"customers.linking.person.dialogSubtitleFor": "Link an existing contact to {{name}}",
|
|
1717
1717
|
"customers.linking.person.dialogTitle": "Link people",
|
|
1718
|
+
"customers.linking.person.role.all": "All",
|
|
1719
|
+
"customers.linking.person.role.budgetHolder": "Budget holder",
|
|
1720
|
+
"customers.linking.person.role.contact": "Contact",
|
|
1721
|
+
"customers.linking.person.role.decisionMaker": "Decision maker",
|
|
1722
|
+
"customers.linking.person.role.stakeholder": "Stakeholder",
|
|
1718
1723
|
"customers.linking.person.searchEmpty": "No matching people found.",
|
|
1719
1724
|
"customers.linking.person.searchPlaceholder": "Search all people…",
|
|
1720
1725
|
"customers.linking.person.sectionLabel": "MATCHING CONTACTS",
|
|
@@ -2658,7 +2663,7 @@
|
|
|
2658
2663
|
"customers.schedule.task.priority.high": "High",
|
|
2659
2664
|
"customers.schedule.task.priority.low": "Low",
|
|
2660
2665
|
"customers.schedule.task.priority.medium": "Medium",
|
|
2661
|
-
"customers.schedule.task.priority.
|
|
2666
|
+
"customers.schedule.task.priority.none": "None",
|
|
2662
2667
|
"customers.schedule.task.priorityLabel": "Priority",
|
|
2663
2668
|
"customers.schedule.task.save": "Save task",
|
|
2664
2669
|
"customers.schedule.task.subtitle": "Capture something to follow up on",
|
|
@@ -1715,6 +1715,11 @@
|
|
|
1715
1715
|
"customers.linking.person.dialogSubtitle": "Vincula un contacto existente a esta empresa",
|
|
1716
1716
|
"customers.linking.person.dialogSubtitleFor": "Vincula un contacto existente a {{name}}",
|
|
1717
1717
|
"customers.linking.person.dialogTitle": "Vincular personas",
|
|
1718
|
+
"customers.linking.person.role.all": "Todos",
|
|
1719
|
+
"customers.linking.person.role.budgetHolder": "Responsable del presupuesto",
|
|
1720
|
+
"customers.linking.person.role.contact": "Contacto",
|
|
1721
|
+
"customers.linking.person.role.decisionMaker": "Responsable de la decisión",
|
|
1722
|
+
"customers.linking.person.role.stakeholder": "Parte interesada",
|
|
1718
1723
|
"customers.linking.person.searchEmpty": "No se encontraron personas.",
|
|
1719
1724
|
"customers.linking.person.searchPlaceholder": "Buscar todas las personas…",
|
|
1720
1725
|
"customers.linking.person.sectionLabel": "CONTACTOS COINCIDENTES",
|
|
@@ -2658,7 +2663,7 @@
|
|
|
2658
2663
|
"customers.schedule.task.priority.high": "Alta",
|
|
2659
2664
|
"customers.schedule.task.priority.low": "Baja",
|
|
2660
2665
|
"customers.schedule.task.priority.medium": "Media",
|
|
2661
|
-
"customers.schedule.task.priority.
|
|
2666
|
+
"customers.schedule.task.priority.none": "Ninguna",
|
|
2662
2667
|
"customers.schedule.task.priorityLabel": "Prioridad",
|
|
2663
2668
|
"customers.schedule.task.save": "Guardar tarea",
|
|
2664
2669
|
"customers.schedule.task.subtitle": "Captura algo para hacer seguimiento",
|
|
@@ -1715,6 +1715,11 @@
|
|
|
1715
1715
|
"customers.linking.person.dialogSubtitle": "기존 연락처를 이 회사에 연결",
|
|
1716
1716
|
"customers.linking.person.dialogSubtitleFor": "기존 연락처를 {{name}}에 연결",
|
|
1717
1717
|
"customers.linking.person.dialogTitle": "사람 연결",
|
|
1718
|
+
"customers.linking.person.role.all": "전체",
|
|
1719
|
+
"customers.linking.person.role.budgetHolder": "예산 담당자",
|
|
1720
|
+
"customers.linking.person.role.contact": "연락 담당자",
|
|
1721
|
+
"customers.linking.person.role.decisionMaker": "의사결정권자",
|
|
1722
|
+
"customers.linking.person.role.stakeholder": "이해관계자",
|
|
1718
1723
|
"customers.linking.person.searchEmpty": "일치하는 사람을 찾을 수 없습니다.",
|
|
1719
1724
|
"customers.linking.person.searchPlaceholder": "전체 사람 검색…",
|
|
1720
1725
|
"customers.linking.person.sectionLabel": "일치하는 연락처",
|
|
@@ -2658,7 +2663,7 @@
|
|
|
2658
2663
|
"customers.schedule.task.priority.high": "높음",
|
|
2659
2664
|
"customers.schedule.task.priority.low": "낮음",
|
|
2660
2665
|
"customers.schedule.task.priority.medium": "보통",
|
|
2661
|
-
"customers.schedule.task.priority.
|
|
2666
|
+
"customers.schedule.task.priority.none": "없음",
|
|
2662
2667
|
"customers.schedule.task.priorityLabel": "우선순위",
|
|
2663
2668
|
"customers.schedule.task.save": "작업 저장",
|
|
2664
2669
|
"customers.schedule.task.subtitle": "후속 조치가 필요한 사항을 기록하세요",
|
|
@@ -1715,6 +1715,11 @@
|
|
|
1715
1715
|
"customers.linking.person.dialogSubtitle": "Powiąż istniejący kontakt z tą firmą",
|
|
1716
1716
|
"customers.linking.person.dialogSubtitleFor": "Powiąż istniejący kontakt z {{name}}",
|
|
1717
1717
|
"customers.linking.person.dialogTitle": "Powiąż osoby",
|
|
1718
|
+
"customers.linking.person.role.all": "Wszystkie",
|
|
1719
|
+
"customers.linking.person.role.budgetHolder": "Dysponent budżetu",
|
|
1720
|
+
"customers.linking.person.role.contact": "Kontakt",
|
|
1721
|
+
"customers.linking.person.role.decisionMaker": "Decydent",
|
|
1722
|
+
"customers.linking.person.role.stakeholder": "Interesariusz",
|
|
1718
1723
|
"customers.linking.person.searchEmpty": "Nie znaleziono pasujących osób.",
|
|
1719
1724
|
"customers.linking.person.searchPlaceholder": "Szukaj wszystkich osób…",
|
|
1720
1725
|
"customers.linking.person.sectionLabel": "PASUJĄCE KONTAKTY",
|
|
@@ -2658,7 +2663,7 @@
|
|
|
2658
2663
|
"customers.schedule.task.priority.high": "Wysoki",
|
|
2659
2664
|
"customers.schedule.task.priority.low": "Niski",
|
|
2660
2665
|
"customers.schedule.task.priority.medium": "Średni",
|
|
2661
|
-
"customers.schedule.task.priority.
|
|
2666
|
+
"customers.schedule.task.priority.none": "Brak",
|
|
2662
2667
|
"customers.schedule.task.priorityLabel": "Priorytet",
|
|
2663
2668
|
"customers.schedule.task.save": "Zapisz zadanie",
|
|
2664
2669
|
"customers.schedule.task.subtitle": "Zapisz coś do późniejszego wykonania",
|
package/src/modules/customers/migrations/Migration20260901120000_reindex_pipeline_stage_colors.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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 `Migration20260519120000_pipeline_stage_color_tones`, which rewrote
|
|
6
|
+
* `customer_dictionary_entries.color` in raw SQL and never notified the query index. On every
|
|
7
|
+
* install that already applied it, `entity_indexes.doc` for `customers:customer_dictionary_entry`
|
|
8
|
+
* still holds the legacy hex chip (`#facc15`, `#a855f7`, …) instead of the semantic tone, and so
|
|
9
|
+
* do the `search_tokens` rows derived from that document. Both migrations are forward-only, so a
|
|
10
|
+
* follow-up migration is the only route to repair existing installs.
|
|
11
|
+
*
|
|
12
|
+
* This migration executes no SQL — the declaration below is its entire payload.
|
|
13
|
+
*/
|
|
14
|
+
export const queryIndexReindexEntityTypes = declareQueryIndexReindex([
|
|
15
|
+
'customers:customer_dictionary_entry',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export class Migration20260901120000_reindex_pipeline_stage_colors extends Migration {
|
|
19
|
+
override up(): void | Promise<void> {}
|
|
20
|
+
|
|
21
|
+
override down(): void | Promise<void> {}
|
|
22
|
+
}
|
package/src/modules/dictionaries/migrations/Migration20260901120000_reindex_dictionary_entries.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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 `Migration20260410171544`, which added `position` and `is_default` to
|
|
6
|
+
* `dictionary_entries` and backfilled them in raw SQL. Documents written before that migration
|
|
7
|
+
* were never rebuilt, so `entity_indexes.doc` for `dictionaries:dictionary_entry` is missing both
|
|
8
|
+
* keys entirely rather than holding a stale value — the same absent emit, a different symptom.
|
|
9
|
+
*
|
|
10
|
+
* This migration executes no SQL — the declaration below is its entire payload.
|
|
11
|
+
*/
|
|
12
|
+
export const queryIndexReindexEntityTypes = declareQueryIndexReindex([
|
|
13
|
+
'dictionaries:dictionary_entry',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export class Migration20260901120000_reindex_dictionary_entries extends Migration {
|
|
17
|
+
override up(): void | Promise<void> {}
|
|
18
|
+
|
|
19
|
+
override down(): void | Promise<void> {}
|
|
20
|
+
}
|
|
@@ -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
|
+
}
|