@open-mercato/core 0.8.1-develop.7228.1.b7ea09d2d3 → 0.8.1-develop.7238.1.3abe669ea5
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 +2 -2
- package/dist/generated/entities/availability_policy/index.js +41 -0
- package/dist/generated/entities/availability_policy/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +4 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +21 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/attachments/data/entities.js +3 -3
- package/dist/modules/attachments/data/entities.js.map +2 -2
- package/dist/modules/availability/acl.js +16 -0
- package/dist/modules/availability/acl.js.map +7 -0
- package/dist/modules/availability/api/check/route.js +95 -0
- package/dist/modules/availability/api/check/route.js.map +7 -0
- package/dist/modules/availability/api/openapi.js +27 -0
- package/dist/modules/availability/api/openapi.js.map +7 -0
- package/dist/modules/availability/api/policies/resolve-preview/route.js +58 -0
- package/dist/modules/availability/api/policies/resolve-preview/route.js.map +7 -0
- package/dist/modules/availability/api/policies/route.js +188 -0
- package/dist/modules/availability/api/policies/route.js.map +7 -0
- package/dist/modules/availability/backend/availability/check/page.js +185 -0
- package/dist/modules/availability/backend/availability/check/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/check/page.meta.js +16 -0
- package/dist/modules/availability/backend/availability/check/page.meta.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/ResolutionPreviewPanel.js +79 -0
- package/dist/modules/availability/backend/availability/policies/ResolutionPreviewPanel.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/ScopeFields.js +53 -0
- package/dist/modules/availability/backend/availability/policies/ScopeFields.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.js +152 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.meta.js +12 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.meta.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/create/page.js +94 -0
- package/dist/modules/availability/backend/availability/policies/create/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/create/page.meta.js +12 -0
- package/dist/modules/availability/backend/availability/policies/create/page.meta.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/formGroups.js +75 -0
- package/dist/modules/availability/backend/availability/policies/formGroups.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/page.js +176 -0
- package/dist/modules/availability/backend/availability/policies/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/page.meta.js +16 -0
- package/dist/modules/availability/backend/availability/policies/page.meta.js.map +7 -0
- package/dist/modules/availability/commands/policies.js +312 -0
- package/dist/modules/availability/commands/policies.js.map +7 -0
- package/dist/modules/availability/commands/scope.js +19 -0
- package/dist/modules/availability/commands/scope.js.map +7 -0
- package/dist/modules/availability/data/entities.js +95 -0
- package/dist/modules/availability/data/entities.js.map +7 -0
- package/dist/modules/availability/data/validators.js +53 -0
- package/dist/modules/availability/data/validators.js.map +7 -0
- package/dist/modules/availability/di.js +41 -0
- package/dist/modules/availability/di.js.map +7 -0
- package/dist/modules/availability/events.js +18 -0
- package/dist/modules/availability/events.js.map +7 -0
- package/dist/modules/availability/index.js +15 -0
- package/dist/modules/availability/index.js.map +7 -0
- package/dist/modules/availability/lib/policyResolution.js +155 -0
- package/dist/modules/availability/lib/policyResolution.js.map +7 -0
- package/dist/modules/availability/lib/tryResolve.js +11 -0
- package/dist/modules/availability/lib/tryResolve.js.map +7 -0
- package/dist/modules/availability/migrations/Migration20260922095844_availability.js +16 -0
- package/dist/modules/availability/migrations/Migration20260922095844_availability.js.map +7 -0
- package/dist/modules/availability/setup.js +12 -0
- package/dist/modules/availability/setup.js.map +7 -0
- package/dist/modules/entities/cli.js +33 -12
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/inbox_ops/api/settings/route.js +7 -32
- package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js.map +2 -2
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js +1 -1
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js.map +2 -2
- package/dist/modules/inbox_ops/lib/ensure-settings.js +47 -0
- package/dist/modules/inbox_ops/lib/ensure-settings.js.map +7 -0
- package/dist/modules/inbox_ops/lib/executionEngine.js.map +1 -1
- package/dist/modules/inbox_ops/lib/executionHelpers.js +0 -23
- package/dist/modules/inbox_ops/lib/executionHelpers.js.map +2 -2
- package/dist/modules/inbox_ops/lib/payloadEnrichment.js +0 -3
- package/dist/modules/inbox_ops/lib/payloadEnrichment.js.map +2 -2
- package/dist/modules/inbox_ops/setup.js +2 -21
- package/dist/modules/inbox_ops/setup.js.map +2 -2
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +1 -1
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +7 -0
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/data/entities.js +3 -1
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/sales/inbox-actions.js +0 -12
- package/dist/modules/sales/inbox-actions.js.map +2 -2
- package/dist/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.js +20 -0
- package/dist/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.js.map +7 -0
- package/dist/modules/wms/di.js +3 -0
- package/dist/modules/wms/di.js.map +2 -2
- package/dist/modules/wms/lib/availabilityCache.js +85 -0
- package/dist/modules/wms/lib/availabilityCache.js.map +7 -0
- package/dist/modules/wms/lib/availabilityCalculation.js +178 -0
- package/dist/modules/wms/lib/availabilityCalculation.js.map +7 -0
- package/dist/modules/wms/lib/availabilityProvider.js +14 -0
- package/dist/modules/wms/lib/availabilityProvider.js.map +7 -0
- package/dist/modules/wms/lib/sqlInClause.js +9 -0
- package/dist/modules/wms/lib/sqlInClause.js.map +7 -0
- package/dist/modules/wms/lib/tryResolve.js +11 -0
- package/dist/modules/wms/lib/tryResolve.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-availability-cache.js +16 -0
- package/dist/modules/wms/subscribers/invalidate-availability-cache.js.map +7 -0
- package/generated/entities/availability_policy/index.ts +19 -0
- package/generated/entities.ids.generated.ts +4 -0
- package/generated/entity-fields-registry.ts +21 -0
- package/package.json +7 -7
- package/src/modules/attachments/data/entities.ts +5 -3
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/availability/acl.ts +12 -0
- package/src/modules/availability/api/check/route.ts +114 -0
- package/src/modules/availability/api/openapi.ts +28 -0
- package/src/modules/availability/api/policies/resolve-preview/route.ts +67 -0
- package/src/modules/availability/api/policies/route.ts +224 -0
- package/src/modules/availability/backend/availability/check/page.meta.ts +12 -0
- package/src/modules/availability/backend/availability/check/page.tsx +223 -0
- package/src/modules/availability/backend/availability/policies/ResolutionPreviewPanel.tsx +106 -0
- package/src/modules/availability/backend/availability/policies/ScopeFields.tsx +55 -0
- package/src/modules/availability/backend/availability/policies/[id]/page.meta.ts +8 -0
- package/src/modules/availability/backend/availability/policies/[id]/page.tsx +182 -0
- package/src/modules/availability/backend/availability/policies/create/page.meta.ts +8 -0
- package/src/modules/availability/backend/availability/policies/create/page.tsx +105 -0
- package/src/modules/availability/backend/availability/policies/formGroups.ts +77 -0
- package/src/modules/availability/backend/availability/policies/page.meta.ts +12 -0
- package/src/modules/availability/backend/availability/policies/page.tsx +196 -0
- package/src/modules/availability/commands/policies.ts +358 -0
- package/src/modules/availability/commands/scope.ts +26 -0
- package/src/modules/availability/data/entities.ts +92 -0
- package/src/modules/availability/data/validators.ts +70 -0
- package/src/modules/availability/di.ts +54 -0
- package/src/modules/availability/events.ts +17 -0
- package/src/modules/availability/i18n/de.json +81 -0
- package/src/modules/availability/i18n/en.json +81 -0
- package/src/modules/availability/i18n/es.json +81 -0
- package/src/modules/availability/i18n/ko.json +81 -0
- package/src/modules/availability/i18n/pl.json +81 -0
- package/src/modules/availability/index.ts +13 -0
- package/src/modules/availability/lib/policyResolution.ts +264 -0
- package/src/modules/availability/lib/tryResolve.ts +8 -0
- package/src/modules/availability/migrations/.snapshot-open-mercato.json +375 -0
- package/src/modules/availability/migrations/Migration20260922095844_availability.ts +13 -0
- package/src/modules/availability/setup.ts +10 -0
- package/src/modules/entities/cli.ts +53 -13
- package/src/modules/inbox_ops/api/settings/route.ts +9 -32
- package/src/modules/inbox_ops/backend/inbox-ops/settings/page.tsx +2 -2
- package/src/modules/inbox_ops/components/proposals/ActionCard.tsx +1 -1
- package/src/modules/inbox_ops/i18n/de.json +1 -1
- package/src/modules/inbox_ops/i18n/en.json +1 -1
- package/src/modules/inbox_ops/i18n/es.json +1 -1
- package/src/modules/inbox_ops/i18n/ko.json +1 -1
- package/src/modules/inbox_ops/i18n/pl.json +1 -1
- package/src/modules/inbox_ops/lib/ensure-settings.ts +77 -0
- package/src/modules/inbox_ops/lib/executionEngine.ts +1 -1
- package/src/modules/inbox_ops/lib/executionHelpers.ts +0 -27
- package/src/modules/inbox_ops/lib/payloadEnrichment.ts +1 -5
- package/src/modules/inbox_ops/setup.ts +2 -21
- package/src/modules/inbox_ops/subscribers/extractionWorker.ts +1 -1
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +13 -0
- package/src/modules/sales/data/entities.ts +2 -0
- package/src/modules/sales/inbox-actions.ts +0 -20
- package/src/modules/sales/migrations/.snapshot-open-mercato.json +20 -0
- package/src/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.ts +74 -0
- package/src/modules/wms/di.ts +6 -0
- package/src/modules/wms/lib/availabilityCache.ts +136 -0
- package/src/modules/wms/lib/availabilityCalculation.ts +289 -0
- package/src/modules/wms/lib/availabilityProvider.ts +29 -0
- package/src/modules/wms/lib/sqlInClause.ts +21 -0
- package/src/modules/wms/lib/tryResolve.ts +8 -0
- package/src/modules/wms/subscribers/invalidate-availability-cache.ts +30 -0
|
@@ -16,6 +16,7 @@ import { createKmsService, type KmsService, type TenantDek } from '@open-mercato
|
|
|
16
16
|
import {
|
|
17
17
|
decryptWithAesGcm,
|
|
18
18
|
decryptWithAesGcmStrict,
|
|
19
|
+
isEncryptedPayloadShape,
|
|
19
20
|
TenantDataEncryptionError,
|
|
20
21
|
TenantDataEncryptionErrorCode,
|
|
21
22
|
} from '@open-mercato/shared/lib/encryption/aes'
|
|
@@ -448,12 +449,6 @@ function resolveMapMeta(
|
|
|
448
449
|
return { entityId, meta, fields, tenantId }
|
|
449
450
|
}
|
|
450
451
|
|
|
451
|
-
function isEncryptedPayload(value: unknown): boolean {
|
|
452
|
-
if (typeof value !== 'string') return false
|
|
453
|
-
const parts = value.split(':')
|
|
454
|
-
return parts.length === 4 && parts[3] === 'v1'
|
|
455
|
-
}
|
|
456
|
-
|
|
457
452
|
function formatValueForColumn(prop: any, value: unknown): unknown {
|
|
458
453
|
if (value === null || value === undefined) return value
|
|
459
454
|
const types = Array.isArray(prop?.columnTypes) ? prop.columnTypes : []
|
|
@@ -550,6 +545,10 @@ const rotateEncryptionKey: ModuleCli = {
|
|
|
550
545
|
}
|
|
551
546
|
|
|
552
547
|
const oldDekCache = new Map<string, TenantDek | null>()
|
|
548
|
+
// Rows this run could not rotate because their ciphertext opens under neither
|
|
549
|
+
// --old-key nor the current tenant key — surfaced in the closing summary so an
|
|
550
|
+
// operator does not have to grep console.warn output for a batch of thousands.
|
|
551
|
+
const unrecoverableRows: Array<{ entityId: string; field: string; rowId: unknown }> = []
|
|
553
552
|
// A dry run must not provision key material. `encryptEntityPayload` creates and
|
|
554
553
|
// persists a tenant DEK in KMS/Vault the first time it runs for a tenant, so a
|
|
555
554
|
// read-only preview would silently mutate KMS state (#5950). Probe read-only
|
|
@@ -599,6 +598,8 @@ const rotateEncryptionKey: ModuleCli = {
|
|
|
599
598
|
const list = Array.isArray(rows) ? rows : []
|
|
600
599
|
if (!list.length) return 0
|
|
601
600
|
const dekAvailable = dryRun ? await hasExistingDek(scope.tenantId) : true
|
|
601
|
+
// The scope pins one tenant, so the current DEK is the same for every row here.
|
|
602
|
+
const currentDek = rotate && oldKms ? await encryptionService.getDek(scope.tenantId) : null
|
|
602
603
|
let updated = 0
|
|
603
604
|
for (const row of list) {
|
|
604
605
|
const payload: Record<string, unknown> = {}
|
|
@@ -607,7 +608,13 @@ const rotateEncryptionKey: ModuleCli = {
|
|
|
607
608
|
const col = resolved?.columnName
|
|
608
609
|
if (!col) continue
|
|
609
610
|
const rawValue = row[col]
|
|
610
|
-
|
|
611
|
+
// Rotate mode only touches values that look like an envelope (candidates to
|
|
612
|
+
// re-key); encrypt mode only touches values that do not (plaintext to seal).
|
|
613
|
+
// A shape-valid envelope encountered in encrypt mode is either already
|
|
614
|
+
// correctly encrypted or sealed under a foreign DEK — either way, handing it
|
|
615
|
+
// to encryptEntityPayload would either no-op or throw WRONG_KEY and abort the
|
|
616
|
+
// whole run (#5951); mirror the filter the update path already applies below.
|
|
617
|
+
if (isEncryptedPayloadShape(rawValue) ? !rotate : rotate) {
|
|
611
618
|
continue
|
|
612
619
|
}
|
|
613
620
|
payload[rule.field] = rawValue
|
|
@@ -628,10 +635,24 @@ const rotateEncryptionKey: ModuleCli = {
|
|
|
628
635
|
}
|
|
629
636
|
for (const rule of fields) {
|
|
630
637
|
const value = payload[rule.field]
|
|
631
|
-
if (typeof value !== 'string' || !
|
|
638
|
+
if (typeof value !== 'string' || !isEncryptedPayloadShape(value)) continue
|
|
632
639
|
const decrypted = decryptWithOldKey(value, oldDek)
|
|
633
|
-
if (decrypted
|
|
634
|
-
|
|
640
|
+
if (decrypted !== null) {
|
|
641
|
+
payload[rule.field] = parseDecryptedFieldValue(decrypted)
|
|
642
|
+
continue
|
|
643
|
+
}
|
|
644
|
+
// The old key did not open it. If the current key does, the row was already
|
|
645
|
+
// rotated (a resumed run) — leave it in the payload, where the service's own
|
|
646
|
+
// already-encrypted check skips it. Otherwise no key we hold can open it, and
|
|
647
|
+
// handing it to the encrypt path would abort the whole batch (#5951). Drop the
|
|
648
|
+
// field so this row is reported and skipped instead.
|
|
649
|
+
if (currentDek && decryptWithAesGcm(value, currentDek.key) !== null) continue
|
|
650
|
+
delete payload[rule.field]
|
|
651
|
+
if (rule.hashField) delete payload[rule.hashField]
|
|
652
|
+
unrecoverableRows.push({ entityId, field: rule.field, rowId: row[pk] })
|
|
653
|
+
console.warn(
|
|
654
|
+
`Skipping ${entityId}.${rule.field} for row ${row[pk]}: its ciphertext opens under neither --old-key nor the current tenant key. Re-run with the key that sealed it.`,
|
|
655
|
+
)
|
|
635
656
|
}
|
|
636
657
|
}
|
|
637
658
|
if (!dekAvailable) {
|
|
@@ -643,7 +664,7 @@ const rotateEncryptionKey: ModuleCli = {
|
|
|
643
664
|
if (!col) return false
|
|
644
665
|
const value = row[col]
|
|
645
666
|
if (value === null || value === undefined) return false
|
|
646
|
-
return rotate ?
|
|
667
|
+
return rotate ? isEncryptedPayloadShape(value) : !isEncryptedPayloadShape(value)
|
|
647
668
|
})
|
|
648
669
|
if (wouldChange) updated += 1
|
|
649
670
|
continue
|
|
@@ -662,7 +683,7 @@ const rotateEncryptionKey: ModuleCli = {
|
|
|
662
683
|
if (!col) continue
|
|
663
684
|
const nextValue = (encrypted as any)[rule.field]
|
|
664
685
|
if (nextValue !== undefined && nextValue !== row[col]) {
|
|
665
|
-
if (!rotate &&
|
|
686
|
+
if (!rotate && isEncryptedPayloadShape(row[col])) continue
|
|
666
687
|
updates[col] = formatValueForColumn(resolved?.prop, nextValue)
|
|
667
688
|
}
|
|
668
689
|
if (rule.hashField) {
|
|
@@ -709,6 +730,20 @@ const rotateEncryptionKey: ModuleCli = {
|
|
|
709
730
|
} else {
|
|
710
731
|
console.log('All mapped entity fields already encrypted for the selected scope.')
|
|
711
732
|
}
|
|
733
|
+
if (unrecoverableRows.length) {
|
|
734
|
+
const maxListed = 20
|
|
735
|
+
console.log(
|
|
736
|
+
`\n⚠️ ${unrecoverableRows.length} field(s) could not be rotated — their ciphertext opens under neither `
|
|
737
|
+
+ '--old-key nor the current tenant key, so they were left untouched. See '
|
|
738
|
+
+ '"Key management" in apps/docs/docs/architecture/data-encryption.mdx for how to resolve these manually.',
|
|
739
|
+
)
|
|
740
|
+
for (const row of unrecoverableRows.slice(0, maxListed)) {
|
|
741
|
+
console.log(` - ${row.entityId}.${row.field} row=${String(row.rowId)}`)
|
|
742
|
+
}
|
|
743
|
+
if (unrecoverableRows.length > maxListed) {
|
|
744
|
+
console.log(` ... and ${unrecoverableRows.length - maxListed} more`)
|
|
745
|
+
}
|
|
746
|
+
}
|
|
712
747
|
},
|
|
713
748
|
}
|
|
714
749
|
|
|
@@ -1201,8 +1236,13 @@ const backfillSystemEncryption: ModuleCli = {
|
|
|
1201
1236
|
const resolved = resolveProperty(meta, rule.field)
|
|
1202
1237
|
if (!resolved.columnName) continue
|
|
1203
1238
|
const rawValue = row[resolved.columnName]
|
|
1239
|
+
// Only plaintext belongs in the payload. An already-encrypted column produced no
|
|
1240
|
+
// update anyway (the service skips what decrypts under the current key), but if the
|
|
1241
|
+
// system key ever changed, passing its ciphertext through would abort the whole
|
|
1242
|
+
// backfill rather than let the row's genuinely-plaintext fields through (#5951).
|
|
1243
|
+
if (rawValue !== null && rawValue !== undefined && isEncryptedPayloadShape(rawValue)) continue
|
|
1204
1244
|
payload[rule.field] = rawValue
|
|
1205
|
-
if (rawValue !== null && rawValue !== undefined
|
|
1245
|
+
if (rawValue !== null && rawValue !== undefined) hasPlaintext = true
|
|
1206
1246
|
if (rule.hashField) {
|
|
1207
1247
|
const resolvedHash = resolveProperty(meta, rule.hashField)
|
|
1208
1248
|
if (resolvedHash.columnName) payload[rule.hashField] = row[resolvedHash.columnName]
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
3
|
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
4
|
-
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
4
|
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
6
5
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
7
|
-
import { InboxSettings } from '../../data/entities'
|
|
8
6
|
import { updateSettingsSchema } from '../../data/validators'
|
|
9
7
|
import { resolveRequestContext, handleRouteError } from '../routeHelpers'
|
|
8
|
+
import { ensureInboxSettings } from '../../lib/ensure-settings'
|
|
10
9
|
import {
|
|
11
10
|
resolveCache,
|
|
12
11
|
createSettingsCacheKey,
|
|
@@ -36,20 +35,10 @@ export async function GET(req: Request) {
|
|
|
36
35
|
}
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
const settings = await
|
|
40
|
-
ctx.em,
|
|
41
|
-
InboxSettings,
|
|
42
|
-
{
|
|
43
|
-
organizationId: ctx.organizationId,
|
|
44
|
-
tenantId: ctx.tenantId,
|
|
45
|
-
deletedAt: null,
|
|
46
|
-
},
|
|
47
|
-
undefined,
|
|
48
|
-
ctx.scope,
|
|
49
|
-
)
|
|
38
|
+
const { settings } = await ensureInboxSettings(ctx.em, ctx.scope)
|
|
50
39
|
|
|
51
40
|
const responseBody = {
|
|
52
|
-
settings:
|
|
41
|
+
settings: {
|
|
53
42
|
id: settings.id,
|
|
54
43
|
inboxAddress: settings.inboxAddress,
|
|
55
44
|
isActive: settings.isActive,
|
|
@@ -57,7 +46,7 @@ export async function GET(req: Request) {
|
|
|
57
46
|
// Surface only whether a per-tenant secret exists — never the value.
|
|
58
47
|
webhookSecretSet: Boolean(settings.webhookSecret),
|
|
59
48
|
updatedAt: settings.updatedAt instanceof Date ? settings.updatedAt.toISOString() : (settings.updatedAt ?? null),
|
|
60
|
-
}
|
|
49
|
+
},
|
|
61
50
|
}
|
|
62
51
|
|
|
63
52
|
if (cache) {
|
|
@@ -88,21 +77,7 @@ export async function PATCH(req: Request) {
|
|
|
88
77
|
return NextResponse.json({ error: 'Invalid request', details: parsed.error.issues }, { status: 400 })
|
|
89
78
|
}
|
|
90
79
|
|
|
91
|
-
const settings = await
|
|
92
|
-
ctx.em,
|
|
93
|
-
InboxSettings,
|
|
94
|
-
{
|
|
95
|
-
organizationId: ctx.organizationId,
|
|
96
|
-
tenantId: ctx.tenantId,
|
|
97
|
-
deletedAt: null,
|
|
98
|
-
},
|
|
99
|
-
undefined,
|
|
100
|
-
ctx.scope,
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
if (!settings) {
|
|
104
|
-
return NextResponse.json({ error: 'Settings not found' }, { status: 404 })
|
|
105
|
-
}
|
|
80
|
+
const { em: settingsEm, settings } = await ensureInboxSettings(ctx.em, ctx.scope)
|
|
106
81
|
|
|
107
82
|
// Optimistic lock: refuse a stale overwrite when two tabs edit the same inbox
|
|
108
83
|
// settings record. Strictly additive — a no-op without the expected-version header.
|
|
@@ -129,7 +104,10 @@ export async function PATCH(req: Request) {
|
|
|
129
104
|
settings.webhookSecret = parsed.data.webhookSecret ? parsed.data.webhookSecret : null
|
|
130
105
|
}
|
|
131
106
|
|
|
132
|
-
|
|
107
|
+
// Flush through `settingsEm` — after a concurrent-bootstrap recovery this
|
|
108
|
+
// is a clean fork, not `ctx.em`, whose unit of work would otherwise retry
|
|
109
|
+
// the losing insert.
|
|
110
|
+
await settingsEm.flush()
|
|
133
111
|
|
|
134
112
|
const cache = resolveCache(ctx.container)
|
|
135
113
|
await runWithCacheTenant(ctx.tenantId, () => invalidateSettingsCache(cache, ctx.tenantId))
|
|
@@ -166,7 +144,6 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
166
144
|
description: 'Updates working language and/or active status',
|
|
167
145
|
responses: [
|
|
168
146
|
{ status: 200, description: 'Updated settings' },
|
|
169
|
-
{ status: 404, description: 'Settings not found' },
|
|
170
147
|
],
|
|
171
148
|
},
|
|
172
149
|
},
|
|
@@ -46,8 +46,8 @@ export default function InboxSettingsPage() {
|
|
|
46
46
|
try {
|
|
47
47
|
const result = await apiCall<{ settings: { inboxAddress?: string; isActive?: boolean; workingLanguage?: string; updatedAt?: string | null } | null }>('/api/inbox_ops/settings')
|
|
48
48
|
if (!cancelled) {
|
|
49
|
-
if (result?.ok
|
|
50
|
-
setSettings(result.result
|
|
49
|
+
if (result?.ok) {
|
|
50
|
+
setSettings(result.result?.settings ?? null)
|
|
51
51
|
} else {
|
|
52
52
|
setError(t('inbox_ops.settings.load_failed', 'Failed to load settings'))
|
|
53
53
|
}
|
|
@@ -31,7 +31,7 @@ export function useDiscrepancyDescriptions(): (description: string, foundValue?:
|
|
|
31
31
|
const t = useT()
|
|
32
32
|
const translations: Record<string, string> = {
|
|
33
33
|
'inbox_ops.discrepancy.desc.no_channel': t('inbox_ops.discrepancy.desc.no_channel', 'No sales channel available. Create a channel in Sales settings before accepting this order.'),
|
|
34
|
-
'inbox_ops.discrepancy.desc.no_currency': t('inbox_ops.discrepancy.desc.no_currency', 'No currency could be resolved for this order. Set a currency code
|
|
34
|
+
'inbox_ops.discrepancy.desc.no_currency': t('inbox_ops.discrepancy.desc.no_currency', 'No currency could be resolved for this order. Set a currency code on the order.'),
|
|
35
35
|
'inbox_ops.discrepancy.desc.product_not_matched': t('inbox_ops.discrepancy.desc.product_not_matched', 'Product could not be matched to any catalog product'),
|
|
36
36
|
'inbox_ops.discrepancy.desc.no_matching_contact': t('inbox_ops.discrepancy.desc.no_matching_contact', 'No matching contact found'),
|
|
37
37
|
'inbox_ops.discrepancy.desc.draft_reply_no_contact': t('inbox_ops.discrepancy.desc.draft_reply_no_contact', 'Draft reply target has no matching contact. Create the contact first.'),
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"inbox_ops.discrepancy.desc.draft_reply_no_contact": "Der Empf\u00e4nger des Antwortentwurfs hat keinen passenden Kontakt. Erstellen Sie zuerst den Kontakt.",
|
|
53
53
|
"inbox_ops.discrepancy.desc.duplicate_order_reference": "Eine Bestellung mit dieser Kundenreferenz existiert bereits",
|
|
54
54
|
"inbox_ops.discrepancy.desc.no_channel": "Kein Vertriebskanal verf\u00fcgbar. Erstellen Sie einen Kanal in den Vertriebseinstellungen, bevor Sie diese Bestellung akzeptieren.",
|
|
55
|
-
"inbox_ops.discrepancy.desc.no_currency": "F\u00fcr diese Bestellung konnte keine W\u00e4hrung ermittelt werden. Legen Sie einen W\u00e4hrungscode
|
|
55
|
+
"inbox_ops.discrepancy.desc.no_currency": "F\u00fcr diese Bestellung konnte keine W\u00e4hrung ermittelt werden. Legen Sie einen W\u00e4hrungscode f\u00fcr die Bestellung fest.",
|
|
56
56
|
"inbox_ops.discrepancy.desc.no_matching_contact": "Kein passender Kontakt gefunden",
|
|
57
57
|
"inbox_ops.discrepancy.desc.product_not_matched": "Produkt konnte keinem Katalogprodukt zugeordnet werden",
|
|
58
58
|
"inbox_ops.discrepancy.expected": "Erwartet",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"inbox_ops.discrepancy.desc.draft_reply_no_contact": "Draft reply target has no matching contact. Create the contact first.",
|
|
53
53
|
"inbox_ops.discrepancy.desc.duplicate_order_reference": "An order with this customer reference already exists",
|
|
54
54
|
"inbox_ops.discrepancy.desc.no_channel": "No sales channel available. Create a channel in Sales settings before accepting this order.",
|
|
55
|
-
"inbox_ops.discrepancy.desc.no_currency": "No currency could be resolved for this order. Set a currency code
|
|
55
|
+
"inbox_ops.discrepancy.desc.no_currency": "No currency could be resolved for this order. Set a currency code on the order.",
|
|
56
56
|
"inbox_ops.discrepancy.desc.no_matching_contact": "No matching contact found",
|
|
57
57
|
"inbox_ops.discrepancy.desc.product_not_matched": "Product could not be matched to any catalog product",
|
|
58
58
|
"inbox_ops.discrepancy.expected": "Expected",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"inbox_ops.discrepancy.desc.draft_reply_no_contact": "El destinatario del borrador no tiene un contacto coincidente. Cree el contacto primero.",
|
|
53
53
|
"inbox_ops.discrepancy.desc.duplicate_order_reference": "Ya existe un pedido con esta referencia de cliente",
|
|
54
54
|
"inbox_ops.discrepancy.desc.no_channel": "No hay canal de ventas disponible. Cree un canal en la configuraci\u00f3n de ventas antes de aceptar este pedido.",
|
|
55
|
-
"inbox_ops.discrepancy.desc.no_currency": "No se pudo determinar la moneda para este pedido. Establezca un c\u00f3digo de moneda
|
|
55
|
+
"inbox_ops.discrepancy.desc.no_currency": "No se pudo determinar la moneda para este pedido. Establezca un c\u00f3digo de moneda para el pedido.",
|
|
56
56
|
"inbox_ops.discrepancy.desc.no_matching_contact": "No se encontr\u00f3 un contacto coincidente",
|
|
57
57
|
"inbox_ops.discrepancy.desc.product_not_matched": "El producto no pudo vincularse a ning\u00fan producto del cat\u00e1logo",
|
|
58
58
|
"inbox_ops.discrepancy.expected": "Esperado",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"inbox_ops.discrepancy.desc.draft_reply_no_contact": "답장 초안의 대상에 일치하는 연락처가 없습니다. 먼저 연락처를 생성하세요.",
|
|
53
53
|
"inbox_ops.discrepancy.desc.duplicate_order_reference": "이 고객 참조 번호로 등록된 주문이 이미 존재합니다",
|
|
54
54
|
"inbox_ops.discrepancy.desc.no_channel": "사용 가능한 판매 채널이 없습니다. 이 주문을 수락하기 전에 판매 설정에서 채널을 생성하세요.",
|
|
55
|
-
"inbox_ops.discrepancy.desc.no_currency": "이 주문의 통화를 확인할 수 없습니다. 통화 코드를
|
|
55
|
+
"inbox_ops.discrepancy.desc.no_currency": "이 주문의 통화를 확인할 수 없습니다. 주문에 통화 코드를 설정하세요.",
|
|
56
56
|
"inbox_ops.discrepancy.desc.no_matching_contact": "일치하는 연락처를 찾을 수 없습니다",
|
|
57
57
|
"inbox_ops.discrepancy.desc.product_not_matched": "상품을 카탈로그 상품과 일치시킬 수 없습니다",
|
|
58
58
|
"inbox_ops.discrepancy.expected": "예상",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"inbox_ops.discrepancy.desc.draft_reply_no_contact": "Odbiorca szkicu odpowiedzi nie ma pasuj\u0105cego kontaktu. Najpierw utw\u00f3rz kontakt.",
|
|
53
53
|
"inbox_ops.discrepancy.desc.duplicate_order_reference": "Zam\u00f3wienie z t\u0105 referencj\u0105 klienta ju\u017c istnieje",
|
|
54
54
|
"inbox_ops.discrepancy.desc.no_channel": "Brak dost\u0119pnego kana\u0142u sprzeda\u017cy. Utw\u00f3rz kana\u0142 w ustawieniach sprzeda\u017cy przed akceptacj\u0105 tego zam\u00f3wienia.",
|
|
55
|
-
"inbox_ops.discrepancy.desc.no_currency": "Nie mo\u017cna ustali\u0107 waluty dla tego zam\u00f3wienia. Ustaw kod waluty
|
|
55
|
+
"inbox_ops.discrepancy.desc.no_currency": "Nie mo\u017cna ustali\u0107 waluty dla tego zam\u00f3wienia. Ustaw kod waluty dla zam\u00f3wienia.",
|
|
56
56
|
"inbox_ops.discrepancy.desc.no_matching_contact": "Nie znaleziono pasuj\u0105cego kontaktu",
|
|
57
57
|
"inbox_ops.discrepancy.desc.product_not_matched": "Nie uda\u0142o si\u0119 dopasowa\u0107 produktu do katalogu",
|
|
58
58
|
"inbox_ops.discrepancy.expected": "Oczekiwano",
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { UniqueConstraintViolationException } from '@mikro-orm/core'
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
|
+
import { InboxSettings } from '../data/entities'
|
|
5
|
+
|
|
6
|
+
export interface EnsureInboxSettingsScope {
|
|
7
|
+
tenantId: string
|
|
8
|
+
organizationId: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface EnsureInboxSettingsResult {
|
|
12
|
+
// The manager the returned `settings` row is actually managed by — a
|
|
13
|
+
// concurrent-bootstrap recovery returns a clean fork, never the manager
|
|
14
|
+
// whose insert just failed, so callers must flush further writes through
|
|
15
|
+
// this manager rather than the one they passed in.
|
|
16
|
+
em: EntityManager
|
|
17
|
+
settings: InboxSettings
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isUniqueViolation(error: unknown): boolean {
|
|
21
|
+
if (error instanceof UniqueConstraintViolationException) return true
|
|
22
|
+
if (!error || typeof error !== 'object') return false
|
|
23
|
+
if ((error as { code?: string }).code === '23505') return true
|
|
24
|
+
const message = (error as { message?: string }).message
|
|
25
|
+
return typeof message === 'string' && message.includes('duplicate key')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function findExisting(em: EntityManager, scope: EnsureInboxSettingsScope) {
|
|
29
|
+
return findOneWithDecryption(
|
|
30
|
+
em,
|
|
31
|
+
InboxSettings,
|
|
32
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },
|
|
33
|
+
undefined,
|
|
34
|
+
scope,
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Lazily bootstraps the tenant's InboxSettings row when missing — e.g. a
|
|
39
|
+
// tenant provisioned before this module existed and never ran setup.ts's
|
|
40
|
+
// onTenantCreated hook. Idempotent: a second call finds the row just created.
|
|
41
|
+
export async function ensureInboxSettings(
|
|
42
|
+
em: EntityManager,
|
|
43
|
+
scope: EnsureInboxSettingsScope,
|
|
44
|
+
): Promise<EnsureInboxSettingsResult> {
|
|
45
|
+
const existing = await findExisting(em, scope)
|
|
46
|
+
if (existing) return { em, settings: existing }
|
|
47
|
+
|
|
48
|
+
const domain = process.env.INBOX_OPS_DOMAIN || 'inbox.mercato.local'
|
|
49
|
+
const slug = scope.organizationId.slice(0, 8)
|
|
50
|
+
const inboxAddress = `ops-${slug}@${domain}`
|
|
51
|
+
const settings = em.create(InboxSettings, {
|
|
52
|
+
tenantId: scope.tenantId,
|
|
53
|
+
organizationId: scope.organizationId,
|
|
54
|
+
inboxAddress,
|
|
55
|
+
isActive: true,
|
|
56
|
+
})
|
|
57
|
+
em.persist(settings)
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await em.flush()
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// Two concurrent bootstraps (e.g. two tabs) race to insert the same
|
|
63
|
+
// deterministic inboxAddress; the loser refetches the winner's row
|
|
64
|
+
// instead of surfacing a raw unique-constraint error to the caller.
|
|
65
|
+
// Recover on a clean fork: `em`'s unit of work still owns the
|
|
66
|
+
// half-persisted `settings` entity scheduled for the failed insert, so
|
|
67
|
+
// flushing `em` again later (e.g. after a caller-side PATCH mutation)
|
|
68
|
+
// would retry that same rejected insert.
|
|
69
|
+
if (!isUniqueViolation(error)) throw error
|
|
70
|
+
const recoveryEm = em.fork()
|
|
71
|
+
const winner = await findExisting(recoveryEm, scope)
|
|
72
|
+
if (!winner) throw error
|
|
73
|
+
return { em: recoveryEm, settings: winner }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { em, settings }
|
|
77
|
+
}
|
|
@@ -27,7 +27,7 @@ export interface CrossModuleEntities {
|
|
|
27
27
|
CustomerEntity: EntityClass<CommonEntityFields & { id: string; kind: string; displayName: string; primaryEmail?: string | null }>
|
|
28
28
|
SalesOrder: EntityClass<CommonEntityFields & { id: string; orderNumber: string; currencyCode: string; comments?: string | null; customerReference?: string | null }>
|
|
29
29
|
SalesShipment: EntityClass<CommonEntityFields & { id: string; order: unknown }>
|
|
30
|
-
SalesChannel: EntityClass<CommonEntityFields & { id: string; name: string;
|
|
30
|
+
SalesChannel: EntityClass<CommonEntityFields & { id: string; name: string; metadata?: Record<string, unknown> | null }>
|
|
31
31
|
Dictionary: EntityClass<CommonEntityFields & { id: string; key: string }>
|
|
32
32
|
DictionaryEntry: EntityClass<CommonEntityFields & { id: string; label: string; value: string; normalizedValue?: string | null; dictionary: unknown }>
|
|
33
33
|
}
|
|
@@ -246,33 +246,6 @@ export async function resolveFirstChannelId(ctx: ExecutionHelperContext): Promis
|
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
export async function resolveChannelCurrency(
|
|
250
|
-
ctx: ExecutionHelperContext,
|
|
251
|
-
channelId: string | null,
|
|
252
|
-
): Promise<string | null> {
|
|
253
|
-
const SalesChannelClass = resolveEntityClass(ctx, 'SalesChannel')
|
|
254
|
-
if (!SalesChannelClass) return null
|
|
255
|
-
|
|
256
|
-
try {
|
|
257
|
-
const where: Record<string, unknown> = {
|
|
258
|
-
tenantId: ctx.tenantId,
|
|
259
|
-
organizationId: ctx.organizationId,
|
|
260
|
-
deletedAt: null,
|
|
261
|
-
}
|
|
262
|
-
if (channelId) where.id = channelId
|
|
263
|
-
const channel = await findOneWithDecryption(
|
|
264
|
-
ctx.em,
|
|
265
|
-
SalesChannelClass,
|
|
266
|
-
where,
|
|
267
|
-
channelId ? undefined : { orderBy: { name: 'ASC' } },
|
|
268
|
-
{ tenantId: ctx.tenantId, organizationId: ctx.organizationId },
|
|
269
|
-
)
|
|
270
|
-
return channel?.currencyCode ?? null
|
|
271
|
-
} catch {
|
|
272
|
-
return null
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
249
|
export async function resolveEffectiveDocumentKind(
|
|
277
250
|
ctx: ExecutionHelperContext,
|
|
278
251
|
channelId: string,
|
|
@@ -17,7 +17,6 @@ interface CatalogProduct {
|
|
|
17
17
|
interface SalesChannelLike {
|
|
18
18
|
id: string
|
|
19
19
|
name: string
|
|
20
|
-
currencyCode?: string
|
|
21
20
|
tenantId?: string
|
|
22
21
|
organizationId?: string
|
|
23
22
|
deletedAt?: Date | null
|
|
@@ -54,7 +53,7 @@ export async function enrichOrderPayload(
|
|
|
54
53
|
const enriched = { ...payload }
|
|
55
54
|
const warnings: string[] = []
|
|
56
55
|
|
|
57
|
-
// 1. Resolve channelId if missing
|
|
56
|
+
// 1. Resolve channelId if missing
|
|
58
57
|
if (ctx.salesChannelClass) {
|
|
59
58
|
try {
|
|
60
59
|
const channelWhere: Record<string, unknown> = {
|
|
@@ -74,9 +73,6 @@ export async function enrichOrderPayload(
|
|
|
74
73
|
)
|
|
75
74
|
if (channel) {
|
|
76
75
|
if (!enriched.channelId) enriched.channelId = channel.id
|
|
77
|
-
if (!enriched.currencyCode && channel.currencyCode) {
|
|
78
|
-
enriched.currencyCode = channel.currencyCode
|
|
79
|
-
}
|
|
80
76
|
} else {
|
|
81
77
|
warnings.push('no_channel_resolved')
|
|
82
78
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'
|
|
2
|
-
import {
|
|
3
|
-
import { InboxSettings } from './data/entities'
|
|
2
|
+
import { ensureInboxSettings } from './lib/ensure-settings'
|
|
4
3
|
|
|
5
4
|
export const setup: ModuleSetupConfig = {
|
|
6
5
|
defaultRoleFeatures: {
|
|
@@ -20,25 +19,7 @@ export const setup: ModuleSetupConfig = {
|
|
|
20
19
|
},
|
|
21
20
|
|
|
22
21
|
async onTenantCreated({ em, tenantId, organizationId }) {
|
|
23
|
-
|
|
24
|
-
em,
|
|
25
|
-
InboxSettings,
|
|
26
|
-
{ tenantId, organizationId, deletedAt: null },
|
|
27
|
-
undefined,
|
|
28
|
-
{ tenantId, organizationId },
|
|
29
|
-
)
|
|
30
|
-
if (!exists) {
|
|
31
|
-
const domain = process.env.INBOX_OPS_DOMAIN || 'inbox.mercato.local'
|
|
32
|
-
const slug = organizationId.slice(0, 8)
|
|
33
|
-
const inboxAddress = `ops-${slug}@${domain}`
|
|
34
|
-
em.persist(em.create(InboxSettings, {
|
|
35
|
-
tenantId,
|
|
36
|
-
organizationId,
|
|
37
|
-
inboxAddress,
|
|
38
|
-
isActive: true,
|
|
39
|
-
}))
|
|
40
|
-
}
|
|
41
|
-
await em.flush()
|
|
22
|
+
await ensureInboxSettings(em, { tenantId, organizationId })
|
|
42
23
|
},
|
|
43
24
|
|
|
44
25
|
async seedDefaults() {},
|
|
@@ -48,7 +48,7 @@ interface ExtractionEntityClasses {
|
|
|
48
48
|
catalogProduct?: EntityClass<{ id: string; name: string; sku?: string | null; tenantId?: string; organizationId?: string; deletedAt?: Date | null }>
|
|
49
49
|
catalogProductPrice?: EntityClass<{ product?: unknown; unitPriceNet?: string | null; unitPriceGross?: string | null; currencyCode?: string | null; tenantId?: string; organizationId?: string; deletedAt?: Date | null; createdAt?: Date }>
|
|
50
50
|
salesOrder?: EntityClass<{ id: string; orderNumber: string; customerReference?: string | null; tenantId?: string; organizationId?: string; deletedAt?: Date | null }>
|
|
51
|
-
salesChannel?: EntityClass<{ id: string; name: string;
|
|
51
|
+
salesChannel?: EntityClass<{ id: string; name: string; tenantId?: string; organizationId?: string; deletedAt?: Date | null }>
|
|
52
52
|
customerAddress?: EntityClass<{ id: string; isPrimary: boolean; tenantId?: string; organizationId?: string; entity?: { id: string } | string; createdAt?: Date }>
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -57,6 +57,8 @@ import {
|
|
|
57
57
|
type AddressValue,
|
|
58
58
|
} from '@open-mercato/core/modules/customers/utils/addressFormat'
|
|
59
59
|
import { AddressEditor, type AddressEditorDraft } from '@open-mercato/core/modules/customers/components/AddressEditor'
|
|
60
|
+
import { extensionPoints as customersExtensionPoints } from '@open-mercato/core/modules/customers/extension-points'
|
|
61
|
+
import { crudFormExtensionSpotId } from '@open-mercato/shared/modules/widgets/extension-points'
|
|
60
62
|
import { useSalesChannelsEnabled } from '../useSalesChannelsEnabled'
|
|
61
63
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
62
64
|
import { SalesOrderDraftLines, createSalesOrderLineDraft, type SalesOrderLineDraft } from './SalesOrderDraftLines'
|
|
@@ -64,6 +66,13 @@ import { normalizeAddressDraft } from './normalizeAddressDraft'
|
|
|
64
66
|
|
|
65
67
|
const logger = createLogger('sales')
|
|
66
68
|
|
|
69
|
+
// Before this change, `CrudForm` auto-derived its injection spot id from
|
|
70
|
+
// `entityIds[0]` (`E.customers.customer_entity`), which resolves to this id.
|
|
71
|
+
// Declaring `injectionSpotId` below on the two quick-create dialogs replaces
|
|
72
|
+
// that auto-derived id with the customers module's declared host — bridge the
|
|
73
|
+
// legacy one for one minor version per BACKWARD_COMPATIBILITY.md §6.
|
|
74
|
+
const CUSTOMER_QUICK_CREATE_LEGACY_INJECTION_SPOT_ID = crudFormExtensionSpotId('customers.customer_entity')
|
|
75
|
+
|
|
67
76
|
type DocumentKind = 'quote' | 'order'
|
|
68
77
|
|
|
69
78
|
type AddressDraft = AddressEditorDraft
|
|
@@ -370,6 +379,8 @@ function CustomerQuickCreate({ t, onCreated }: CustomerQuickCreateProps) {
|
|
|
370
379
|
submitLabel={t('common.save', 'Save')}
|
|
371
380
|
cancelHref={undefined}
|
|
372
381
|
onSubmit={(values) => handlePersonCreate(values)}
|
|
382
|
+
injectionSpotId={customersExtensionPoints.hosts.personForm.spotId}
|
|
383
|
+
legacyInjectionSpotId={CUSTOMER_QUICK_CREATE_LEGACY_INJECTION_SPOT_ID}
|
|
373
384
|
entityIds={[E.customers.customer_entity, E.customers.customer_person_profile]}
|
|
374
385
|
/>
|
|
375
386
|
</div>
|
|
@@ -404,6 +415,8 @@ function CustomerQuickCreate({ t, onCreated }: CustomerQuickCreateProps) {
|
|
|
404
415
|
submitLabel={t('common.save', 'Save')}
|
|
405
416
|
cancelHref={undefined}
|
|
406
417
|
onSubmit={(values) => handleCompanyCreate(values)}
|
|
418
|
+
injectionSpotId={customersExtensionPoints.hosts.companyForm.spotId}
|
|
419
|
+
legacyInjectionSpotId={CUSTOMER_QUICK_CREATE_LEGACY_INJECTION_SPOT_ID}
|
|
407
420
|
entityIds={[E.customers.customer_entity, E.customers.customer_company_profile]}
|
|
408
421
|
/>
|
|
409
422
|
</div>
|
|
@@ -1759,6 +1759,8 @@ export class SalesPayment {
|
|
|
1759
1759
|
|
|
1760
1760
|
@Entity({ tableName: 'sales_payment_allocations' })
|
|
1761
1761
|
@Index({ name: 'sales_payment_allocations_scope_idx', properties: ['payment', 'organizationId', 'tenantId'] })
|
|
1762
|
+
@Index({ name: 'sales_payment_allocations_order_idx', properties: ['order'] })
|
|
1763
|
+
@Index({ name: 'sales_payment_allocations_invoice_idx', properties: ['invoice'] })
|
|
1762
1764
|
export class SalesPaymentAllocation {
|
|
1763
1765
|
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
1764
1766
|
id!: string
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
buildSourceMetadata,
|
|
9
9
|
resolveOrderByReference,
|
|
10
10
|
resolveFirstChannelId,
|
|
11
|
-
resolveChannelCurrency,
|
|
12
11
|
resolveEffectiveDocumentKind,
|
|
13
12
|
resolveShipmentStatusEntryId,
|
|
14
13
|
resolveCustomerEntityIdByEmail,
|
|
@@ -288,23 +287,6 @@ async function executeUpdateShipmentAction(
|
|
|
288
287
|
return { createdEntityId: shipment.id, createdEntityType: 'sales_shipment' }
|
|
289
288
|
}
|
|
290
289
|
|
|
291
|
-
// ---------------------------------------------------------------------------
|
|
292
|
-
// Normalization helper for order/quote payloads
|
|
293
|
-
// ---------------------------------------------------------------------------
|
|
294
|
-
|
|
295
|
-
async function normalizeOrderPayload(
|
|
296
|
-
payload: Record<string, unknown>,
|
|
297
|
-
ctx: InboxActionExecutionContext,
|
|
298
|
-
): Promise<Record<string, unknown>> {
|
|
299
|
-
if (!payload.currencyCode) {
|
|
300
|
-
const hCtx = asHelperContext(ctx)
|
|
301
|
-
const channelId = typeof payload.channelId === 'string' ? payload.channelId : null
|
|
302
|
-
const resolved = await resolveChannelCurrency(hCtx, channelId)
|
|
303
|
-
if (resolved) payload.currencyCode = resolved
|
|
304
|
-
}
|
|
305
|
-
return payload
|
|
306
|
-
}
|
|
307
|
-
|
|
308
290
|
// ---------------------------------------------------------------------------
|
|
309
291
|
// Exported action definitions
|
|
310
292
|
// ---------------------------------------------------------------------------
|
|
@@ -323,7 +305,6 @@ export const inboxActions: InboxActionDefinition[] = [
|
|
|
323
305
|
'For create_order / create_quote: each line item MUST have "productName" (the product name goes here, NOT in "description"). Include currencyCode and customerName.',
|
|
324
306
|
'For create_order / create_quote: extract shippingAddress and billingAddress as structured objects when addresses are mentioned. Parse street, city, postal code, country from the text. Do NOT put address data in notes.',
|
|
325
307
|
],
|
|
326
|
-
normalizePayload: normalizeOrderPayload,
|
|
327
308
|
execute: (action, ctx) => executeCreateDocumentAction(action, ctx, 'order'),
|
|
328
309
|
},
|
|
329
310
|
{
|
|
@@ -332,7 +313,6 @@ export const inboxActions: InboxActionDefinition[] = [
|
|
|
332
313
|
payloadSchema: orderPayloadSchema,
|
|
333
314
|
label: 'Create Quote',
|
|
334
315
|
promptSchema: '(shared with create_order)',
|
|
335
|
-
normalizePayload: normalizeOrderPayload,
|
|
336
316
|
execute: (action, ctx) => executeCreateDocumentAction(action, ctx, 'quote'),
|
|
337
317
|
},
|
|
338
318
|
{
|
|
@@ -6232,6 +6232,26 @@
|
|
|
6232
6232
|
"keyName": "sales_payment_allocations_scope_idx",
|
|
6233
6233
|
"primary": false,
|
|
6234
6234
|
"unique": false
|
|
6235
|
+
},
|
|
6236
|
+
{
|
|
6237
|
+
"columnNames": [
|
|
6238
|
+
"order_id"
|
|
6239
|
+
],
|
|
6240
|
+
"composite": false,
|
|
6241
|
+
"constraint": false,
|
|
6242
|
+
"keyName": "sales_payment_allocations_order_idx",
|
|
6243
|
+
"primary": false,
|
|
6244
|
+
"unique": false
|
|
6245
|
+
},
|
|
6246
|
+
{
|
|
6247
|
+
"columnNames": [
|
|
6248
|
+
"invoice_id"
|
|
6249
|
+
],
|
|
6250
|
+
"composite": false,
|
|
6251
|
+
"constraint": false,
|
|
6252
|
+
"keyName": "sales_payment_allocations_invoice_idx",
|
|
6253
|
+
"primary": false,
|
|
6254
|
+
"unique": false
|
|
6235
6255
|
}
|
|
6236
6256
|
],
|
|
6237
6257
|
"checks": [],
|