@open-mercato/core 0.6.3-develop.3525.3.cc18b8a43f → 0.6.3-develop.3574.1.f3c5dcf345
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/helpers/integration/queue-runner.js +15 -3
- package/dist/helpers/integration/queue-runner.js.map +2 -2
- package/dist/helpers/integration/queue.js +1 -1
- package/dist/helpers/integration/queue.js.map +2 -2
- package/dist/modules/auth/api/roles/acl/route.js +37 -1
- package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
- package/dist/modules/auth/api/roles/route.js +36 -2
- package/dist/modules/auth/api/roles/route.js.map +2 -2
- package/dist/modules/auth/api/users/acl/route.js +36 -1
- package/dist/modules/auth/api/users/acl/route.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +89 -11
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/backend/roles/[id]/edit/page.js +32 -14
- package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/backend/users/[id]/edit/page.js +1 -1
- package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/backend/users/create/page.js +1 -1
- package/dist/modules/auth/backend/users/create/page.js.map +2 -2
- package/dist/modules/auth/backend/users/roleOptions.js +1 -1
- package/dist/modules/auth/backend/users/roleOptions.js.map +2 -2
- package/dist/modules/auth/commands/roles.js +8 -0
- package/dist/modules/auth/commands/roles.js.map +2 -2
- package/dist/modules/auth/commands/users.js +3 -0
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/auth/components/AclEditor.js +53 -26
- package/dist/modules/auth/components/AclEditor.js.map +2 -2
- package/dist/modules/auth/lib/grantChecks.js +97 -2
- package/dist/modules/auth/lib/grantChecks.js.map +2 -2
- package/dist/modules/business_rules/lib/expression-evaluator.js +9 -7
- package/dist/modules/business_rules/lib/expression-evaluator.js.map +2 -2
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +41 -12
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +104 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/data/validators.js +20 -1
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/translations/components/TranslationManager.js +9 -4
- package/dist/modules/translations/components/TranslationManager.js.map +2 -2
- package/dist/modules/workflows/lib/event-trigger-service.js +23 -11
- package/dist/modules/workflows/lib/event-trigger-service.js.map +2 -2
- package/jest.config.cjs +1 -0
- package/package.json +7 -7
- package/src/helpers/integration/queue-runner.ts +21 -2
- package/src/helpers/integration/queue.ts +1 -1
- package/src/modules/auth/api/roles/acl/route.ts +39 -1
- package/src/modules/auth/api/roles/route.ts +38 -2
- package/src/modules/auth/api/users/acl/route.ts +41 -1
- package/src/modules/auth/api/users/route.ts +92 -11
- package/src/modules/auth/backend/roles/[id]/edit/page.tsx +35 -13
- package/src/modules/auth/backend/users/[id]/edit/page.tsx +1 -1
- package/src/modules/auth/backend/users/create/page.tsx +1 -1
- package/src/modules/auth/backend/users/roleOptions.ts +2 -1
- package/src/modules/auth/commands/roles.ts +8 -0
- package/src/modules/auth/commands/users.ts +3 -0
- package/src/modules/auth/components/AclEditor.tsx +50 -24
- package/src/modules/auth/lib/grantChecks.ts +116 -2
- package/src/modules/business_rules/lib/expression-evaluator.ts +9 -13
- package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +38 -12
- package/src/modules/sales/commands/documents.ts +116 -0
- package/src/modules/sales/data/validators.ts +38 -0
- package/src/modules/sales/i18n/de.json +2 -0
- package/src/modules/sales/i18n/en.json +2 -0
- package/src/modules/sales/i18n/es.json +2 -0
- package/src/modules/sales/i18n/pl.json +2 -0
- package/src/modules/translations/components/TranslationManager.tsx +9 -4
- package/src/modules/workflows/lib/event-trigger-service.ts +24 -11
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
|
|
2
2
|
import { CrudHttpError, forbidden } from '@open-mercato/shared/lib/crud/errors'
|
|
3
3
|
import { hasFeature } from '@open-mercato/shared/security/features'
|
|
4
|
-
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
|
-
import { Role, RoleAcl } from '@open-mercato/core/modules/auth/data/entities'
|
|
4
|
+
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
|
+
import { Role, RoleAcl, UserAcl, UserRole } from '@open-mercato/core/modules/auth/data/entities'
|
|
6
6
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
7
7
|
|
|
8
8
|
type ActorAcl = {
|
|
@@ -33,6 +33,16 @@ type FeatureGrantCheckInput = GrantCheckContext & {
|
|
|
33
33
|
organizations?: string[] | null
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
type SuperAdminUserTargetInput = GrantCheckContext & {
|
|
37
|
+
targetUserId: string
|
|
38
|
+
actorIsSuperAdmin?: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type SuperAdminRoleTargetInput = GrantCheckContext & {
|
|
42
|
+
targetRoleId: string
|
|
43
|
+
actorIsSuperAdmin?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
37
47
|
|
|
38
48
|
export async function assertActorCanGrantRoleTokens(input: RoleTokenGrantCheckInput): Promise<Role[]> {
|
|
@@ -99,6 +109,110 @@ export function normalizeGrantFeatureList(features: unknown): string[] {
|
|
|
99
109
|
return normalizeStringList(features)
|
|
100
110
|
}
|
|
101
111
|
|
|
112
|
+
export async function assertActorCanModifySuperAdminUserTarget(input: SuperAdminUserTargetInput): Promise<void> {
|
|
113
|
+
const actorIsSuperAdmin = await resolveActorIsSuperAdmin(input)
|
|
114
|
+
if (actorIsSuperAdmin) return
|
|
115
|
+
const targetIsSuperAdmin = await isUserEffectivelySuperAdmin(input.em, input.targetUserId)
|
|
116
|
+
if (targetIsSuperAdmin) {
|
|
117
|
+
throw forbidden('Only super administrators can modify super administrator accounts.')
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function assertActorCanModifySuperAdminRoleTarget(input: SuperAdminRoleTargetInput): Promise<void> {
|
|
122
|
+
const actorIsSuperAdmin = await resolveActorIsSuperAdmin(input)
|
|
123
|
+
if (actorIsSuperAdmin) return
|
|
124
|
+
const targetIsSuperAdmin = await isRoleEffectivelySuperAdmin(input.em, input.targetRoleId)
|
|
125
|
+
if (targetIsSuperAdmin) {
|
|
126
|
+
throw forbidden('Only super administrators can modify super administrator roles.')
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function resolveActorIsSuperAdmin(input: GrantCheckContext & { actorIsSuperAdmin?: boolean }): Promise<boolean> {
|
|
131
|
+
if (typeof input.actorIsSuperAdmin === 'boolean') return input.actorIsSuperAdmin
|
|
132
|
+
const acl = await loadActorAcl(input)
|
|
133
|
+
return acl.isSuperAdmin
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function isUserEffectivelySuperAdmin(em: EntityManager, userId: string): Promise<boolean> {
|
|
137
|
+
const directGrant = await em.findOne(
|
|
138
|
+
UserAcl,
|
|
139
|
+
{ user: userId as unknown, isSuperAdmin: true } as FilterQuery<UserAcl>,
|
|
140
|
+
)
|
|
141
|
+
if (directGrant && (directGrant as { isSuperAdmin?: boolean }).isSuperAdmin === true) return true
|
|
142
|
+
const links = await findWithDecryption(
|
|
143
|
+
em,
|
|
144
|
+
UserRole,
|
|
145
|
+
{ user: userId as unknown } as FilterQuery<UserRole>,
|
|
146
|
+
{ populate: ['role'] },
|
|
147
|
+
{ tenantId: null, organizationId: null },
|
|
148
|
+
)
|
|
149
|
+
const roleIds = (Array.isArray(links) ? links : [])
|
|
150
|
+
.map((link) => {
|
|
151
|
+
const role = (link as { role?: { id?: unknown } | string | null }).role
|
|
152
|
+
if (!role) return null
|
|
153
|
+
if (typeof role === 'string') return role
|
|
154
|
+
return role.id ? String(role.id) : null
|
|
155
|
+
})
|
|
156
|
+
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
157
|
+
if (!roleIds.length) return false
|
|
158
|
+
const roleGrant = await em.findOne(
|
|
159
|
+
RoleAcl,
|
|
160
|
+
{ role: { $in: roleIds } as unknown, isSuperAdmin: true } as FilterQuery<RoleAcl>,
|
|
161
|
+
)
|
|
162
|
+
return !!roleGrant && (roleGrant as { isSuperAdmin?: boolean }).isSuperAdmin === true
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function isRoleEffectivelySuperAdmin(em: EntityManager, roleId: string): Promise<boolean> {
|
|
166
|
+
const grant = await em.findOne(
|
|
167
|
+
RoleAcl,
|
|
168
|
+
{ role: roleId as unknown, isSuperAdmin: true } as FilterQuery<RoleAcl>,
|
|
169
|
+
)
|
|
170
|
+
return !!grant && (grant as { isSuperAdmin?: boolean }).isSuperAdmin === true
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function listSuperAdminUserIds(em: EntityManager, tenantId: string | null): Promise<Set<string>> {
|
|
174
|
+
const ids = new Set<string>()
|
|
175
|
+
const userAclFilter: Record<string, unknown> = { isSuperAdmin: true }
|
|
176
|
+
if (tenantId) userAclFilter.tenantId = tenantId
|
|
177
|
+
const userAcls = await em.find(UserAcl, userAclFilter as FilterQuery<UserAcl>)
|
|
178
|
+
for (const acl of userAcls) {
|
|
179
|
+
const userRef = (acl as { user?: { id?: unknown } | string | null }).user
|
|
180
|
+
const userId = userRef && typeof userRef === 'object'
|
|
181
|
+
? userRef.id
|
|
182
|
+
: userRef
|
|
183
|
+
if (userId) ids.add(String(userId))
|
|
184
|
+
}
|
|
185
|
+
const roleAcls = await em.find(
|
|
186
|
+
RoleAcl,
|
|
187
|
+
{ isSuperAdmin: true } as FilterQuery<RoleAcl>,
|
|
188
|
+
)
|
|
189
|
+
const roleIds = roleAcls
|
|
190
|
+
.map((acl) => {
|
|
191
|
+
const roleRef = (acl as { role?: { id?: unknown } | string | null }).role
|
|
192
|
+
if (!roleRef) return null
|
|
193
|
+
if (typeof roleRef === 'string') return roleRef
|
|
194
|
+
return roleRef.id ? String(roleRef.id) : null
|
|
195
|
+
})
|
|
196
|
+
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
197
|
+
if (roleIds.length) {
|
|
198
|
+
const links = await findWithDecryption(
|
|
199
|
+
em,
|
|
200
|
+
UserRole,
|
|
201
|
+
{ role: { $in: roleIds } as unknown } as FilterQuery<UserRole>,
|
|
202
|
+
{},
|
|
203
|
+
{ tenantId: null, organizationId: null },
|
|
204
|
+
)
|
|
205
|
+
for (const link of Array.isArray(links) ? links : []) {
|
|
206
|
+
const userRef = (link as { user?: { id?: unknown } | string | null }).user
|
|
207
|
+
const userId = userRef && typeof userRef === 'object'
|
|
208
|
+
? userRef.id
|
|
209
|
+
: userRef
|
|
210
|
+
if (userId) ids.add(String(userId))
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return ids
|
|
214
|
+
}
|
|
215
|
+
|
|
102
216
|
async function loadActorAcl(input: GrantCheckContext): Promise<ActorAcl> {
|
|
103
217
|
const actorUserId = normalizeNullableString(input.actorUserId)
|
|
104
218
|
if (!actorUserId) throw forbidden('Not authorized to grant ACL privileges.')
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComparisonOperator, LogicalOperator } from '../data/validators'
|
|
2
|
+
import { testLinearRegex } from '@open-mercato/shared/lib/regex/linear'
|
|
2
3
|
import { getNestedValue, resolveSpecialValue } from './value-resolver'
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -319,11 +320,8 @@ function endsWith(str: any, suffix: any): boolean {
|
|
|
319
320
|
return String(str).endsWith(String(suffix))
|
|
320
321
|
}
|
|
321
322
|
|
|
322
|
-
/**
|
|
323
|
-
* Security limits for regex operations
|
|
324
|
-
*/
|
|
325
|
-
const REGEX_TIMEOUT_MS = 100
|
|
326
323
|
const MAX_REGEX_LENGTH = 200
|
|
324
|
+
const MAX_REGEX_INPUT_LENGTH = 10_000
|
|
327
325
|
|
|
328
326
|
/**
|
|
329
327
|
* Check if string matches regex pattern (with ReDoS protection)
|
|
@@ -345,18 +343,16 @@ function matches(str: any, pattern: any): boolean {
|
|
|
345
343
|
return false
|
|
346
344
|
}
|
|
347
345
|
|
|
348
|
-
const regex = new RegExp(patternStr)
|
|
349
346
|
const testStr = String(str)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const startTime = Date.now()
|
|
353
|
-
const result = regex.test(testStr)
|
|
354
|
-
|
|
355
|
-
if (Date.now() - startTime > REGEX_TIMEOUT_MS) {
|
|
356
|
-
throw new Error('Regex execution timeout - potential ReDoS pattern detected')
|
|
347
|
+
if (testStr.length > MAX_REGEX_INPUT_LENGTH) {
|
|
348
|
+
return false
|
|
357
349
|
}
|
|
358
350
|
|
|
359
|
-
|
|
351
|
+
const result = testLinearRegex(patternStr, testStr, {
|
|
352
|
+
maxPatternLength: MAX_REGEX_LENGTH,
|
|
353
|
+
maxInputLength: MAX_REGEX_INPUT_LENGTH,
|
|
354
|
+
})
|
|
355
|
+
return result.ok ? result.matched : false
|
|
360
356
|
} catch (error) {
|
|
361
357
|
// Log the error for debugging but don't expose to user
|
|
362
358
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
@@ -32,6 +32,7 @@ type UserResponse = {
|
|
|
32
32
|
type BaseProps = {
|
|
33
33
|
tenantId?: string | null
|
|
34
34
|
organizationId?: string | null
|
|
35
|
+
preserveOnTenantChange?: boolean
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
type RoleProps = BaseProps & {
|
|
@@ -92,7 +93,7 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
92
93
|
(widget: WidgetCatalogItem) => resolveWidgetText(t, widget.id, 'description', widget.description || ''),
|
|
93
94
|
[t],
|
|
94
95
|
)
|
|
95
|
-
const { kind, targetId, tenantId, organizationId } = props
|
|
96
|
+
const { kind, targetId, tenantId, organizationId, preserveOnTenantChange = false } = props
|
|
96
97
|
const [catalog, setCatalog] = React.useState<WidgetCatalogItem[]>([])
|
|
97
98
|
const [loading, setLoading] = React.useState(true)
|
|
98
99
|
const [saving, setSaving] = React.useState(false)
|
|
@@ -136,10 +137,16 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
136
137
|
setCatalog(mapped)
|
|
137
138
|
}, [])
|
|
138
139
|
|
|
139
|
-
const
|
|
140
|
+
const tenantIdRef = React.useRef(tenantId)
|
|
141
|
+
React.useEffect(() => { tenantIdRef.current = tenantId }, [tenantId])
|
|
142
|
+
const organizationIdRef = React.useRef(organizationId)
|
|
143
|
+
React.useEffect(() => { organizationIdRef.current = organizationId }, [organizationId])
|
|
144
|
+
const hasMountedRef = React.useRef(false)
|
|
145
|
+
|
|
146
|
+
const loadRoleData = React.useCallback(async (forTenantId: string | null | undefined, forOrganizationId: string | null | undefined) => {
|
|
140
147
|
const params = new URLSearchParams({ roleId: targetId })
|
|
141
|
-
if (
|
|
142
|
-
if (
|
|
148
|
+
if (forTenantId) params.set('tenantId', forTenantId)
|
|
149
|
+
if (forOrganizationId) params.set('organizationId', forOrganizationId)
|
|
143
150
|
const data = await readApiResultOrThrow<RoleResponse>(
|
|
144
151
|
`/api/dashboards/roles/widgets?${params.toString()}`,
|
|
145
152
|
undefined,
|
|
@@ -151,12 +158,12 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
151
158
|
setMode('override')
|
|
152
159
|
setOriginalMode('override')
|
|
153
160
|
setEffective(ids)
|
|
154
|
-
}, [
|
|
161
|
+
}, [targetId])
|
|
155
162
|
|
|
156
|
-
const loadUserData = React.useCallback(async () => {
|
|
163
|
+
const loadUserData = React.useCallback(async (forTenantId: string | null | undefined, forOrganizationId: string | null | undefined) => {
|
|
157
164
|
const params = new URLSearchParams({ userId: targetId })
|
|
158
|
-
if (
|
|
159
|
-
if (
|
|
165
|
+
if (forTenantId) params.set('tenantId', forTenantId)
|
|
166
|
+
if (forOrganizationId) params.set('organizationId', forOrganizationId)
|
|
160
167
|
const data = await readApiResultOrThrow<UserResponse>(
|
|
161
168
|
`/api/dashboards/users/widgets?${params.toString()}`,
|
|
162
169
|
undefined,
|
|
@@ -168,7 +175,7 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
168
175
|
setMode(data.mode || 'inherit')
|
|
169
176
|
setOriginalMode(data.mode || 'inherit')
|
|
170
177
|
setEffective(Array.isArray(data.effectiveWidgetIds) ? data.effectiveWidgetIds : [])
|
|
171
|
-
}, [
|
|
178
|
+
}, [targetId])
|
|
172
179
|
|
|
173
180
|
React.useEffect(() => {
|
|
174
181
|
let cancelled = false
|
|
@@ -177,21 +184,40 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
177
184
|
setError(null)
|
|
178
185
|
try {
|
|
179
186
|
await loadCatalog()
|
|
180
|
-
if (kind === 'role') await loadRoleData()
|
|
181
|
-
else await loadUserData()
|
|
187
|
+
if (kind === 'role') await loadRoleData(tenantIdRef.current, organizationIdRef.current)
|
|
188
|
+
else await loadUserData(tenantIdRef.current, organizationIdRef.current)
|
|
182
189
|
} catch (err) {
|
|
183
190
|
console.error('Failed to load widget visibility data', err)
|
|
184
191
|
if (!cancelled) {
|
|
185
192
|
setError(tRef.current('dashboards.widgets.error.load', 'Unable to load widget configuration.'))
|
|
186
193
|
}
|
|
187
194
|
} finally {
|
|
188
|
-
if (!cancelled)
|
|
195
|
+
if (!cancelled) {
|
|
196
|
+
setLoading(false)
|
|
197
|
+
hasMountedRef.current = true
|
|
198
|
+
}
|
|
189
199
|
}
|
|
190
200
|
}
|
|
191
201
|
load()
|
|
192
202
|
return () => { cancelled = true }
|
|
193
203
|
}, [kind, loadCatalog, loadRoleData, loadUserData])
|
|
194
204
|
|
|
205
|
+
React.useEffect(() => {
|
|
206
|
+
if (!hasMountedRef.current) return
|
|
207
|
+
if (preserveOnTenantChange) return
|
|
208
|
+
let cancelled = false
|
|
209
|
+
async function refetch() {
|
|
210
|
+
try {
|
|
211
|
+
if (kind === 'role') await loadRoleData(tenantId, organizationId)
|
|
212
|
+
else await loadUserData(tenantId, organizationId)
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.error('Failed to reload widget visibility data', err)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
refetch()
|
|
218
|
+
return () => { cancelled = true }
|
|
219
|
+
}, [tenantId, organizationId, kind, loadRoleData, loadUserData, preserveOnTenantChange])
|
|
220
|
+
|
|
195
221
|
const toggle = React.useCallback((id: string) => {
|
|
196
222
|
setSelected((prev) => (prev.includes(id) ? prev.filter((value) => value !== id) : [...prev, id]))
|
|
197
223
|
}, [])
|
|
@@ -67,6 +67,9 @@ import {
|
|
|
67
67
|
} from "../../customers/data/entities";
|
|
68
68
|
import {
|
|
69
69
|
enforceAdjustmentSign,
|
|
70
|
+
validateReturnAdjustmentWithinRemaining,
|
|
71
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
|
|
72
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE,
|
|
70
73
|
quoteCreateSchema,
|
|
71
74
|
quoteLineCreateSchema,
|
|
72
75
|
quoteAdjustmentCreateSchema,
|
|
@@ -7384,6 +7387,63 @@ const orderAdjustmentUpsertCommand: CommandHandler<
|
|
|
7384
7387
|
shippingMethodCode: order.shippingMethodCode ?? null,
|
|
7385
7388
|
paymentMethodCode: order.paymentMethodCode ?? null,
|
|
7386
7389
|
});
|
|
7390
|
+
const effectiveAdjustment = nextAdjustments.find(
|
|
7391
|
+
(adj) => adj.id === adjustmentId,
|
|
7392
|
+
);
|
|
7393
|
+
if (effectiveAdjustment?.kind === "return") {
|
|
7394
|
+
const baselineAdjustments = nextAdjustments.filter(
|
|
7395
|
+
(adj) => adj.id !== adjustmentId,
|
|
7396
|
+
);
|
|
7397
|
+
const baselineCalculation =
|
|
7398
|
+
await salesCalculationService.calculateDocumentTotals({
|
|
7399
|
+
documentKind: "order",
|
|
7400
|
+
lines: calcLines,
|
|
7401
|
+
adjustments: baselineAdjustments,
|
|
7402
|
+
context: calculationContext,
|
|
7403
|
+
existingTotals: resolveExistingPaymentTotals(order),
|
|
7404
|
+
});
|
|
7405
|
+
const issues = validateReturnAdjustmentWithinRemaining({
|
|
7406
|
+
kind: "return",
|
|
7407
|
+
amountNet:
|
|
7408
|
+
effectiveAdjustment.amountNet ?? effectiveAdjustment.amountGross ?? 0,
|
|
7409
|
+
amountGross:
|
|
7410
|
+
effectiveAdjustment.amountGross ?? effectiveAdjustment.amountNet ?? 0,
|
|
7411
|
+
remainingNet: Number(
|
|
7412
|
+
baselineCalculation.totals?.grandTotalNetAmount ?? 0,
|
|
7413
|
+
),
|
|
7414
|
+
remainingGross: Number(
|
|
7415
|
+
baselineCalculation.totals?.grandTotalGrossAmount ?? 0,
|
|
7416
|
+
),
|
|
7417
|
+
});
|
|
7418
|
+
if (issues.length > 0) {
|
|
7419
|
+
const { translate } = await resolveTranslations();
|
|
7420
|
+
const grossIssue = issues.find((issue) => issue.path === "amountGross");
|
|
7421
|
+
const netIssue = issues.find((issue) => issue.path === "amountNet");
|
|
7422
|
+
const fieldErrors: Record<string, string> = {};
|
|
7423
|
+
if (grossIssue) {
|
|
7424
|
+
fieldErrors.amountGross = translate(
|
|
7425
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
|
|
7426
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
|
|
7427
|
+
);
|
|
7428
|
+
}
|
|
7429
|
+
if (netIssue) {
|
|
7430
|
+
fieldErrors.amountNet = translate(
|
|
7431
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalNet",
|
|
7432
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE,
|
|
7433
|
+
);
|
|
7434
|
+
}
|
|
7435
|
+
throw new CrudHttpError(400, {
|
|
7436
|
+
error:
|
|
7437
|
+
fieldErrors.amountGross ??
|
|
7438
|
+
fieldErrors.amountNet ??
|
|
7439
|
+
translate(
|
|
7440
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
|
|
7441
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
|
|
7442
|
+
),
|
|
7443
|
+
fieldErrors,
|
|
7444
|
+
});
|
|
7445
|
+
}
|
|
7446
|
+
}
|
|
7387
7447
|
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
7388
7448
|
documentKind: "order",
|
|
7389
7449
|
lines: calcLines,
|
|
@@ -7766,6 +7826,62 @@ const quoteAdjustmentUpsertCommand: CommandHandler<
|
|
|
7766
7826
|
shippingMethodCode: quote.shippingMethodCode ?? null,
|
|
7767
7827
|
paymentMethodCode: quote.paymentMethodCode ?? null,
|
|
7768
7828
|
});
|
|
7829
|
+
const effectiveAdjustment = nextAdjustments.find(
|
|
7830
|
+
(adj) => adj.id === adjustmentId,
|
|
7831
|
+
);
|
|
7832
|
+
if (effectiveAdjustment?.kind === "return") {
|
|
7833
|
+
const baselineAdjustments = nextAdjustments.filter(
|
|
7834
|
+
(adj) => adj.id !== adjustmentId,
|
|
7835
|
+
);
|
|
7836
|
+
const baselineCalculation =
|
|
7837
|
+
await salesCalculationService.calculateDocumentTotals({
|
|
7838
|
+
documentKind: "quote",
|
|
7839
|
+
lines: calcLines,
|
|
7840
|
+
adjustments: baselineAdjustments,
|
|
7841
|
+
context: calculationContext,
|
|
7842
|
+
});
|
|
7843
|
+
const issues = validateReturnAdjustmentWithinRemaining({
|
|
7844
|
+
kind: "return",
|
|
7845
|
+
amountNet:
|
|
7846
|
+
effectiveAdjustment.amountNet ?? effectiveAdjustment.amountGross ?? 0,
|
|
7847
|
+
amountGross:
|
|
7848
|
+
effectiveAdjustment.amountGross ?? effectiveAdjustment.amountNet ?? 0,
|
|
7849
|
+
remainingNet: Number(
|
|
7850
|
+
baselineCalculation.totals?.grandTotalNetAmount ?? 0,
|
|
7851
|
+
),
|
|
7852
|
+
remainingGross: Number(
|
|
7853
|
+
baselineCalculation.totals?.grandTotalGrossAmount ?? 0,
|
|
7854
|
+
),
|
|
7855
|
+
});
|
|
7856
|
+
if (issues.length > 0) {
|
|
7857
|
+
const { translate } = await resolveTranslations();
|
|
7858
|
+
const grossIssue = issues.find((issue) => issue.path === "amountGross");
|
|
7859
|
+
const netIssue = issues.find((issue) => issue.path === "amountNet");
|
|
7860
|
+
const fieldErrors: Record<string, string> = {};
|
|
7861
|
+
if (grossIssue) {
|
|
7862
|
+
fieldErrors.amountGross = translate(
|
|
7863
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
|
|
7864
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
|
|
7865
|
+
);
|
|
7866
|
+
}
|
|
7867
|
+
if (netIssue) {
|
|
7868
|
+
fieldErrors.amountNet = translate(
|
|
7869
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalNet",
|
|
7870
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE,
|
|
7871
|
+
);
|
|
7872
|
+
}
|
|
7873
|
+
throw new CrudHttpError(400, {
|
|
7874
|
+
error:
|
|
7875
|
+
fieldErrors.amountGross ??
|
|
7876
|
+
fieldErrors.amountNet ??
|
|
7877
|
+
translate(
|
|
7878
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross",
|
|
7879
|
+
RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE,
|
|
7880
|
+
),
|
|
7881
|
+
fieldErrors,
|
|
7882
|
+
});
|
|
7883
|
+
}
|
|
7884
|
+
}
|
|
7769
7885
|
const calculation = await salesCalculationService.calculateDocumentTotals({
|
|
7770
7886
|
documentKind: "quote",
|
|
7771
7887
|
lines: calcLines,
|
|
@@ -523,6 +523,44 @@ export const enforceAdjustmentSign = (
|
|
|
523
523
|
*/
|
|
524
524
|
export const enforceReturnAdjustmentSign = enforceAdjustmentSign
|
|
525
525
|
|
|
526
|
+
export const RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE =
|
|
527
|
+
'Return adjustment amountNet exceeds the remaining grand total. Reduce the amount or remove existing returns first.'
|
|
528
|
+
export const RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE =
|
|
529
|
+
'Return adjustment amountGross exceeds the remaining grand total. Reduce the amount or remove existing returns first.'
|
|
530
|
+
|
|
531
|
+
export type ReturnAdjustmentRemainingCheck = {
|
|
532
|
+
kind?: string | null
|
|
533
|
+
amountNet?: number | null
|
|
534
|
+
amountGross?: number | null
|
|
535
|
+
remainingNet: number
|
|
536
|
+
remainingGross: number
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export type ReturnAdjustmentRemainingIssue = {
|
|
540
|
+
path: 'amountNet' | 'amountGross'
|
|
541
|
+
message: string
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Inclusive: abs(amount) === remaining is allowed. Tiny epsilon absorbs
|
|
545
|
+
// floating-point rounding from upstream tax/rate adjustments.
|
|
546
|
+
const RETURN_REMAINING_EPSILON = 0.005
|
|
547
|
+
|
|
548
|
+
export const validateReturnAdjustmentWithinRemaining = (
|
|
549
|
+
value: ReturnAdjustmentRemainingCheck
|
|
550
|
+
): ReturnAdjustmentRemainingIssue[] => {
|
|
551
|
+
if (value.kind !== 'return') return []
|
|
552
|
+
const absNet = typeof value.amountNet === 'number' ? Math.abs(value.amountNet) : 0
|
|
553
|
+
const absGross = typeof value.amountGross === 'number' ? Math.abs(value.amountGross) : 0
|
|
554
|
+
const issues: ReturnAdjustmentRemainingIssue[] = []
|
|
555
|
+
if (absGross > value.remainingGross + RETURN_REMAINING_EPSILON) {
|
|
556
|
+
issues.push({ path: 'amountGross', message: RETURN_ADJUSTMENT_EXCEEDS_REMAINING_GROSS_MESSAGE })
|
|
557
|
+
}
|
|
558
|
+
if (absNet > value.remainingNet + RETURN_REMAINING_EPSILON) {
|
|
559
|
+
issues.push({ path: 'amountNet', message: RETURN_ADJUSTMENT_EXCEEDS_REMAINING_NET_MESSAGE })
|
|
560
|
+
}
|
|
561
|
+
return issues
|
|
562
|
+
}
|
|
563
|
+
|
|
526
564
|
export const orderAdjustmentCreateSchema = scoped.extend({
|
|
527
565
|
orderId: uuid(),
|
|
528
566
|
orderLineId: uuid().optional(),
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
"audit_logs.resource_kind.sales.payment": "Zahlung",
|
|
5
5
|
"audit_logs.resource_kind.sales.shipment": "Versand",
|
|
6
6
|
"customers~sales.nav.group": "Vertrieb",
|
|
7
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross": "Der Bruttobetrag der Rücksendeanpassung überschreitet den verbleibenden Gesamtbetrag. Bitte verringern Sie den Betrag oder entfernen Sie bestehende Rücksendungen.",
|
|
8
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalNet": "Der Nettobetrag der Rücksendeanpassung überschreitet den verbleibenden Gesamtbetrag. Bitte verringern Sie den Betrag oder entfernen Sie bestehende Rücksendungen.",
|
|
7
9
|
"sales.audit.channels.create": "Vertriebskanal erstellen",
|
|
8
10
|
"sales.audit.channels.delete": "Vertriebskanal löschen",
|
|
9
11
|
"sales.audit.channels.update": "Vertriebskanal aktualisieren",
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
"audit_logs.resource_kind.sales.payment": "Payment",
|
|
5
5
|
"audit_logs.resource_kind.sales.shipment": "Shipment",
|
|
6
6
|
"customers~sales.nav.group": "Sales",
|
|
7
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross": "Return adjustment amountGross exceeds the remaining grand total. Reduce the amount or remove existing returns first.",
|
|
8
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalNet": "Return adjustment amountNet exceeds the remaining grand total. Reduce the amount or remove existing returns first.",
|
|
7
9
|
"sales.audit.channels.create": "Create sales channel",
|
|
8
10
|
"sales.audit.channels.delete": "Delete sales channel",
|
|
9
11
|
"sales.audit.channels.update": "Update sales channel",
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
"audit_logs.resource_kind.sales.payment": "Pago",
|
|
5
5
|
"audit_logs.resource_kind.sales.shipment": "Envío",
|
|
6
6
|
"customers~sales.nav.group": "Ventas",
|
|
7
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross": "El importe bruto del ajuste de devolución supera el total restante. Reduzca el importe o elimine las devoluciones existentes.",
|
|
8
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalNet": "El importe neto del ajuste de devolución supera el total restante. Reduzca el importe o elimine las devoluciones existentes.",
|
|
7
9
|
"sales.audit.channels.create": "Crear canal de ventas",
|
|
8
10
|
"sales.audit.channels.delete": "Eliminar canal de ventas",
|
|
9
11
|
"sales.audit.channels.update": "Actualizar canal de ventas",
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
"audit_logs.resource_kind.sales.payment": "Płatność",
|
|
5
5
|
"audit_logs.resource_kind.sales.shipment": "Wysyłka",
|
|
6
6
|
"customers~sales.nav.group": "Sprzedaż",
|
|
7
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalGross": "Kwota brutto korekty zwrotu przekracza pozostałą wartość zamówienia. Zmniejsz kwotę lub usuń istniejące zwroty.",
|
|
8
|
+
"sales.adjustments.error.returnExceedsRemainingGrandTotalNet": "Kwota netto korekty zwrotu przekracza pozostałą wartość zamówienia. Zmniejsz kwotę lub usuń istniejące zwroty.",
|
|
7
9
|
"sales.audit.channels.create": "Utwórz kanał sprzedaży",
|
|
8
10
|
"sales.audit.channels.delete": "Usuń kanał sprzedaży",
|
|
9
11
|
"sales.audit.channels.update": "Zaktualizuj kanał sprzedaży",
|
|
@@ -69,6 +69,7 @@ export function TranslationManager({
|
|
|
69
69
|
const [editedTranslations, setEditedTranslations] = React.useState<Record<string, Record<string, string>>>({})
|
|
70
70
|
const editedTranslationsRef = React.useRef<Record<string, Record<string, string>>>({})
|
|
71
71
|
const [hasUserEdited, setHasUserEdited] = React.useState(false)
|
|
72
|
+
const hasUserEditedRef = React.useRef(false)
|
|
72
73
|
|
|
73
74
|
const entityType = isEmbedded ? (propEntityType ?? '') : selectedEntityType
|
|
74
75
|
const recordId = isEmbedded ? (propRecordId ?? '') : selectedRecordId
|
|
@@ -191,11 +192,11 @@ export function TranslationManager({
|
|
|
191
192
|
|
|
192
193
|
React.useEffect(() => {
|
|
193
194
|
const sig = translationSignature
|
|
194
|
-
if (sig === lastTranslationSignatureRef.current &&
|
|
195
|
+
if (sig === lastTranslationSignatureRef.current && hasUserEditedRef.current) return
|
|
195
196
|
lastTranslationSignatureRef.current = sig
|
|
196
197
|
|
|
197
198
|
if (!translationData?.translations) {
|
|
198
|
-
if (!
|
|
199
|
+
if (!hasUserEditedRef.current) {
|
|
199
200
|
editedTranslationsRef.current = {}
|
|
200
201
|
setEditedTranslations({})
|
|
201
202
|
}
|
|
@@ -210,11 +211,11 @@ export function TranslationManager({
|
|
|
210
211
|
parsed[locale][key] = typeof val === 'string' ? val : ''
|
|
211
212
|
}
|
|
212
213
|
}
|
|
213
|
-
if (!
|
|
214
|
+
if (!hasUserEditedRef.current) {
|
|
214
215
|
editedTranslationsRef.current = parsed
|
|
215
216
|
setEditedTranslations(parsed)
|
|
216
217
|
}
|
|
217
|
-
}, [translationSignature, translationData
|
|
218
|
+
}, [translationSignature, translationData])
|
|
218
219
|
|
|
219
220
|
const mutation = useMutation({
|
|
220
221
|
mutationFn: async () => {
|
|
@@ -252,6 +253,7 @@ export function TranslationManager({
|
|
|
252
253
|
},
|
|
253
254
|
onSuccess: () => {
|
|
254
255
|
flash(t('translations.manager.flash.saved', 'Translations saved'), 'success')
|
|
256
|
+
hasUserEditedRef.current = false
|
|
255
257
|
setHasUserEdited(false)
|
|
256
258
|
void refetchTranslation()
|
|
257
259
|
},
|
|
@@ -262,6 +264,7 @@ export function TranslationManager({
|
|
|
262
264
|
})
|
|
263
265
|
|
|
264
266
|
const updateFieldValue = (locale: string, fieldKey: string, value: string) => {
|
|
267
|
+
hasUserEditedRef.current = true
|
|
265
268
|
setHasUserEdited(true)
|
|
266
269
|
const next = {
|
|
267
270
|
...editedTranslationsRef.current,
|
|
@@ -288,6 +291,7 @@ export function TranslationManager({
|
|
|
288
291
|
value={selectedRecordId}
|
|
289
292
|
onChange={(next) => {
|
|
290
293
|
setSelectedRecordId(next)
|
|
294
|
+
hasUserEditedRef.current = false
|
|
291
295
|
setHasUserEdited(false)
|
|
292
296
|
}}
|
|
293
297
|
placeholder={t('translations.manager.searchRecords', 'Search records...')}
|
|
@@ -482,6 +486,7 @@ export function TranslationManager({
|
|
|
482
486
|
onChange={(next) => {
|
|
483
487
|
setSelectedEntityType(next)
|
|
484
488
|
setSelectedRecordId('')
|
|
489
|
+
hasUserEditedRef.current = false
|
|
485
490
|
setHasUserEdited(false)
|
|
486
491
|
}}
|
|
487
492
|
placeholder={t('translations.manager.placeholder', 'Select an entity')}
|
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { EntityManager } from '@mikro-orm/core'
|
|
9
|
+
import type { EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'
|
|
9
10
|
import type { AwilixContainer } from 'awilix'
|
|
10
11
|
import type { CacheService } from '@open-mercato/cache'
|
|
11
12
|
import { matchEventPattern } from '@open-mercato/shared/lib/events/patterns'
|
|
13
|
+
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
14
|
+
import { testLinearRegex } from '@open-mercato/shared/lib/regex/linear'
|
|
12
15
|
import {
|
|
13
16
|
WorkflowEventTrigger,
|
|
14
17
|
WorkflowDefinition,
|
|
@@ -257,11 +260,12 @@ function evaluateCondition(condition: TriggerFilterCondition, payload: Record<st
|
|
|
257
260
|
if (typeof value !== 'string' || typeof expected !== 'string') return false
|
|
258
261
|
if (value.length > MAX_WORKFLOW_REGEX_INPUT_LENGTH) return false
|
|
259
262
|
if (!isSafeWorkflowRegexPattern(expected)) return false
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
{
|
|
264
|
+
const result = testLinearRegex(expected, value, {
|
|
265
|
+
maxPatternLength: MAX_WORKFLOW_REGEX_PATTERN_LENGTH,
|
|
266
|
+
maxInputLength: MAX_WORKFLOW_REGEX_INPUT_LENGTH,
|
|
267
|
+
})
|
|
268
|
+
return result.ok ? result.matched : false
|
|
265
269
|
}
|
|
266
270
|
|
|
267
271
|
default:
|
|
@@ -325,7 +329,9 @@ async function loadLegacyTriggers(
|
|
|
325
329
|
tenantId: string,
|
|
326
330
|
organizationId: string
|
|
327
331
|
): Promise<UnifiedTrigger[]> {
|
|
328
|
-
const
|
|
332
|
+
const postgresEm = em as unknown as PostgreSqlEntityManager
|
|
333
|
+
const legacyTriggers = await findWithDecryption(
|
|
334
|
+
postgresEm,
|
|
329
335
|
WorkflowEventTrigger,
|
|
330
336
|
{
|
|
331
337
|
tenantId,
|
|
@@ -335,16 +341,19 @@ async function loadLegacyTriggers(
|
|
|
335
341
|
},
|
|
336
342
|
{
|
|
337
343
|
orderBy: { priority: 'DESC', createdAt: 'ASC' },
|
|
338
|
-
}
|
|
344
|
+
},
|
|
345
|
+
{ tenantId, organizationId },
|
|
339
346
|
)
|
|
340
347
|
|
|
341
348
|
// Get definitions for these triggers to get workflowId
|
|
342
349
|
const definitionIds = [...new Set(legacyTriggers.map(t => t.workflowDefinitionId))]
|
|
343
|
-
const definitions = definitionIds.length > 0 ? await
|
|
350
|
+
const definitions = definitionIds.length > 0 ? await findWithDecryption(postgresEm, WorkflowDefinition, {
|
|
344
351
|
id: { $in: definitionIds },
|
|
352
|
+
tenantId,
|
|
353
|
+
organizationId,
|
|
345
354
|
enabled: true,
|
|
346
355
|
deletedAt: null,
|
|
347
|
-
}) : []
|
|
356
|
+
}, {}, { tenantId, organizationId }) : []
|
|
348
357
|
const definitionMap = new Map(definitions.map(d => [d.id, d]))
|
|
349
358
|
|
|
350
359
|
return legacyTriggers
|
|
@@ -379,15 +388,19 @@ async function loadEmbeddedTriggers(
|
|
|
379
388
|
tenantId: string,
|
|
380
389
|
organizationId: string
|
|
381
390
|
): Promise<UnifiedTrigger[]> {
|
|
391
|
+
const postgresEm = em as unknown as PostgreSqlEntityManager
|
|
382
392
|
// Load all enabled definitions that may have triggers
|
|
383
|
-
const definitions = await
|
|
393
|
+
const definitions = await findWithDecryption(
|
|
394
|
+
postgresEm,
|
|
384
395
|
WorkflowDefinition,
|
|
385
396
|
{
|
|
386
397
|
tenantId,
|
|
387
398
|
organizationId,
|
|
388
399
|
enabled: true,
|
|
389
400
|
deletedAt: null,
|
|
390
|
-
}
|
|
401
|
+
},
|
|
402
|
+
{},
|
|
403
|
+
{ tenantId, organizationId },
|
|
391
404
|
)
|
|
392
405
|
|
|
393
406
|
const triggers: UnifiedTrigger[] = []
|