@open-mercato/core 0.6.4-develop.4239.1.4a264a5828 → 0.6.4-develop.4264.1.53368d85fe
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/authFixtures.js +70 -1
- package/dist/helpers/integration/authFixtures.js.map +2 -2
- package/dist/helpers/integration/dbFixtures.js +98 -0
- package/dist/helpers/integration/dbFixtures.js.map +7 -0
- package/dist/modules/business_rules/api/execute/route.js +2 -1
- package/dist/modules/business_rules/api/execute/route.js.map +2 -2
- package/dist/modules/business_rules/api/rules/route.js +10 -0
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/business_rules/cli.js +6 -0
- package/dist/modules/business_rules/cli.js.map +2 -2
- package/dist/modules/business_rules/lib/rule-engine.js +116 -9
- package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
- package/dist/modules/business_rules/subscribers/crud-rule-trigger.js +3 -2
- package/dist/modules/business_rules/subscribers/crud-rule-trigger.js.map +2 -2
- package/dist/modules/catalog/api/products/route.js +21 -4
- package/dist/modules/catalog/api/products/route.js.map +2 -2
- package/dist/modules/catalog/lib/pricing.js +6 -0
- package/dist/modules/catalog/lib/pricing.js.map +2 -2
- package/dist/modules/catalog/services/catalogPricingService.js +5 -1
- package/dist/modules/catalog/services/catalogPricingService.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/events/stream.js +1 -0
- package/dist/modules/customer_accounts/api/portal/events/stream.js.map +2 -2
- package/dist/modules/customers/api/activities/route.js +15 -2
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/api/comments/route.js +15 -3
- package/dist/modules/customers/api/comments/route.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/people/route.js +2 -4
- package/dist/modules/customers/api/companies/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +2 -4
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/companies/route.js +2 -4
- package/dist/modules/customers/api/deals/[id]/companies/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js +2 -4
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +2 -9
- package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/stats/route.js +2 -9
- package/dist/modules/customers/api/deals/[id]/stats/route.js.map +2 -2
- package/dist/modules/customers/api/entity-roles-factory.js +2 -8
- package/dist/modules/customers/api/entity-roles-factory.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/context.js +2 -4
- package/dist/modules/customers/api/people/[id]/companies/context.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +2 -4
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/route.js +2 -4
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/directory/utils/organizationScopeGuard.js +22 -0
- package/dist/modules/directory/utils/organizationScopeGuard.js.map +7 -0
- package/dist/modules/workflows/cli.js +8 -0
- package/dist/modules/workflows/cli.js.map +2 -2
- package/dist/modules/workflows/lib/seeds.js +8 -4
- package/dist/modules/workflows/lib/seeds.js.map +2 -2
- package/dist/modules/workflows/setup.js +3 -1
- package/dist/modules/workflows/setup.js.map +2 -2
- package/package.json +7 -7
- package/src/helpers/integration/authFixtures.ts +98 -0
- package/src/helpers/integration/dbFixtures.ts +144 -0
- package/src/modules/business_rules/api/execute/route.ts +2 -1
- package/src/modules/business_rules/api/rules/route.ts +10 -0
- package/src/modules/business_rules/cli.ts +6 -0
- package/src/modules/business_rules/lib/rule-engine.ts +163 -9
- package/src/modules/business_rules/subscribers/crud-rule-trigger.ts +3 -2
- package/src/modules/catalog/api/products/route.ts +23 -4
- package/src/modules/catalog/lib/pricing.ts +9 -0
- package/src/modules/catalog/services/catalogPricingService.ts +6 -0
- package/src/modules/customer_accounts/api/portal/events/stream.ts +6 -0
- package/src/modules/customers/api/activities/route.ts +16 -5
- package/src/modules/customers/api/comments/route.ts +15 -5
- package/src/modules/customers/api/companies/[id]/people/route.ts +2 -4
- package/src/modules/customers/api/companies/[id]/route.ts +2 -5
- package/src/modules/customers/api/deals/[id]/companies/route.ts +2 -4
- package/src/modules/customers/api/deals/[id]/people/route.ts +2 -4
- package/src/modules/customers/api/deals/[id]/route.ts +2 -9
- package/src/modules/customers/api/deals/[id]/stats/route.ts +2 -9
- package/src/modules/customers/api/entity-roles-factory.ts +2 -12
- package/src/modules/customers/api/people/[id]/companies/context.ts +2 -5
- package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +2 -5
- package/src/modules/customers/api/people/[id]/route.ts +2 -5
- package/src/modules/directory/utils/organizationScopeGuard.ts +39 -0
- package/src/modules/staff/i18n/de.json +23 -0
- package/src/modules/staff/i18n/en.json +23 -0
- package/src/modules/staff/i18n/es.json +23 -0
- package/src/modules/staff/i18n/pl.json +23 -0
- package/src/modules/workflows/cli.ts +8 -0
- package/src/modules/workflows/lib/seeds.ts +13 -3
- package/src/modules/workflows/setup.ts +3 -1
|
@@ -2,6 +2,10 @@ import type { ModuleCli } from '@open-mercato/shared/modules/registry'
|
|
|
2
2
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
3
3
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
4
|
import { BusinessRule } from './data/entities'
|
|
5
|
+
import {
|
|
6
|
+
invalidateBusinessRuleDiscoveryCache,
|
|
7
|
+
resolveBusinessRuleDiscoveryCache,
|
|
8
|
+
} from './lib/rule-engine'
|
|
5
9
|
import * as fs from 'fs'
|
|
6
10
|
import * as path from 'path'
|
|
7
11
|
|
|
@@ -39,6 +43,7 @@ const seedGuardRules: ModuleCli = {
|
|
|
39
43
|
try {
|
|
40
44
|
const { resolve } = await createRequestContainer()
|
|
41
45
|
const em = resolve<EntityManager>('em')
|
|
46
|
+
const cache = resolveBusinessRuleDiscoveryCache(resolve)
|
|
42
47
|
|
|
43
48
|
// Read guard rules from workflows examples
|
|
44
49
|
const rulesPath = path.join(__dirname, '../workflows/examples', 'guard-rules-example.json')
|
|
@@ -70,6 +75,7 @@ const seedGuardRules: ModuleCli = {
|
|
|
70
75
|
})
|
|
71
76
|
|
|
72
77
|
await em.persist(rule).flush()
|
|
78
|
+
await invalidateBusinessRuleDiscoveryCache(cache, tenantId, organizationId)
|
|
73
79
|
console.log(` ✓ Seeded guard rule: ${rule.ruleName}`)
|
|
74
80
|
seededCount++
|
|
75
81
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/core'
|
|
2
|
+
import { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'
|
|
2
3
|
import type { EventBus } from '@open-mercato/events'
|
|
3
4
|
import { BusinessRule, RuleExecutionLog, type RuleType } from '../data/entities'
|
|
4
5
|
import * as ruleEvaluator from './rule-evaluator'
|
|
@@ -22,6 +23,7 @@ const EXECUTION_RESULT_FAILURE = 'FAILURE'
|
|
|
22
23
|
const MAX_RULES_PER_EXECUTION = 100
|
|
23
24
|
const MAX_SINGLE_RULE_TIMEOUT_MS = 30000 // 30 seconds
|
|
24
25
|
const MAX_TOTAL_EXECUTION_TIMEOUT_MS = 60000 // 60 seconds
|
|
26
|
+
const RULE_DISCOVERY_CACHE_TTL = 5 * 60 * 1000
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* Rule execution context
|
|
@@ -86,8 +88,138 @@ export interface RuleDiscoveryOptions {
|
|
|
86
88
|
ruleType?: RuleType
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
type CachedRuleDiscovery = {
|
|
92
|
+
ruleIds: string[]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type RuleDiscoveryCache = Pick<CacheStrategy, 'get' | 'set' | 'deleteByTags'>
|
|
96
|
+
|
|
97
|
+
export type RuleDiscoveryCacheOptions = {
|
|
98
|
+
cache?: RuleDiscoveryCache | null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeCachePart(value: string | null | undefined): string {
|
|
102
|
+
return encodeURIComponent(value?.trim() || '*')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getRuleDiscoveryCacheKey(options: RuleDiscoveryOptions): string {
|
|
106
|
+
return [
|
|
107
|
+
normalizeCachePart(options.tenantId),
|
|
108
|
+
normalizeCachePart(options.organizationId),
|
|
109
|
+
normalizeCachePart(options.entityType),
|
|
110
|
+
normalizeCachePart(options.eventType),
|
|
111
|
+
normalizeCachePart(options.ruleType),
|
|
112
|
+
].join(':')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isCachedRuleDiscovery(value: unknown): value is CachedRuleDiscovery {
|
|
116
|
+
if (!value || typeof value !== 'object') return false
|
|
117
|
+
const ruleIds = (value as CachedRuleDiscovery).ruleIds
|
|
118
|
+
return Array.isArray(ruleIds) && ruleIds.every((entry) => typeof entry === 'string')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function isRuleDiscoveryCache(value: unknown): value is RuleDiscoveryCache {
|
|
122
|
+
if (!value || typeof value !== 'object') return false
|
|
123
|
+
const candidate = value as Partial<RuleDiscoveryCache>
|
|
124
|
+
return (
|
|
125
|
+
typeof candidate.get === 'function'
|
|
126
|
+
&& typeof candidate.set === 'function'
|
|
127
|
+
&& typeof candidate.deleteByTags === 'function'
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function resolveBusinessRuleDiscoveryCache(resolve: (token: string) => unknown): RuleDiscoveryCache | null {
|
|
132
|
+
try {
|
|
133
|
+
const cache = resolve('cache')
|
|
134
|
+
return isRuleDiscoveryCache(cache) ? cache : null
|
|
135
|
+
} catch {
|
|
136
|
+
return null
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getRuleDiscoveryCacheTags(options: Pick<RuleDiscoveryOptions, 'organizationId'>): string[] {
|
|
141
|
+
return [
|
|
142
|
+
'business_rules:discovery',
|
|
143
|
+
`business_rules:discovery:organization:${options.organizationId}`,
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function getCachedRuleDiscovery(
|
|
148
|
+
cache: RuleDiscoveryCache | null | undefined,
|
|
149
|
+
options: RuleDiscoveryOptions,
|
|
150
|
+
): Promise<CachedRuleDiscovery | null> {
|
|
151
|
+
if (!cache) return null
|
|
152
|
+
|
|
153
|
+
let value: unknown
|
|
154
|
+
try {
|
|
155
|
+
value = await runWithCacheTenant(options.tenantId, () =>
|
|
156
|
+
cache.get(getRuleDiscoveryCacheKey(options))
|
|
157
|
+
)
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.warn('[business_rules] Failed to read rule discovery cache:', error)
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return isCachedRuleDiscovery(value) ? value : null
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function cacheRuleDiscovery(
|
|
167
|
+
cache: RuleDiscoveryCache | null | undefined,
|
|
168
|
+
options: RuleDiscoveryOptions,
|
|
169
|
+
rules: BusinessRule[],
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
if (!cache) return
|
|
172
|
+
|
|
173
|
+
const ruleIds = rules
|
|
174
|
+
.map((rule) => rule.id)
|
|
175
|
+
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
|
176
|
+
|
|
177
|
+
if (ruleIds.length !== rules.length) return
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await runWithCacheTenant(options.tenantId, () =>
|
|
181
|
+
cache.set(
|
|
182
|
+
getRuleDiscoveryCacheKey(options),
|
|
183
|
+
{ ruleIds },
|
|
184
|
+
{
|
|
185
|
+
ttl: RULE_DISCOVERY_CACHE_TTL,
|
|
186
|
+
tags: getRuleDiscoveryCacheTags(options),
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.warn('[business_rules] Failed to write rule discovery cache:', error)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function invalidateBusinessRuleDiscoveryCache(
|
|
196
|
+
cache: RuleDiscoveryCache | null | undefined,
|
|
197
|
+
tenantId?: string | null,
|
|
198
|
+
organizationId?: string | null,
|
|
199
|
+
): Promise<void> {
|
|
200
|
+
if (!cache) return
|
|
201
|
+
|
|
202
|
+
const normalizedTenantId = tenantId?.trim()
|
|
203
|
+
const normalizedOrganizationId = organizationId?.trim()
|
|
204
|
+
|
|
205
|
+
if (!normalizedTenantId) {
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const tags = normalizedOrganizationId
|
|
210
|
+
? [`business_rules:discovery:organization:${normalizedOrganizationId}`]
|
|
211
|
+
: ['business_rules:discovery']
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
await runWithCacheTenant(normalizedTenantId, () => cache.deleteByTags(tags))
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.warn('[business_rules] Failed to invalidate rule discovery cache:', error)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
89
220
|
export type RuleEngineExecutionOptions = {
|
|
90
221
|
eventBus?: Pick<EventBus, 'emitEvent'> | null
|
|
222
|
+
cache?: RuleDiscoveryCache | null
|
|
91
223
|
}
|
|
92
224
|
|
|
93
225
|
type RuleExecutionFailedPayload = {
|
|
@@ -213,7 +345,7 @@ export async function executeRules(
|
|
|
213
345
|
eventType: context.eventType,
|
|
214
346
|
tenantId: context.tenantId,
|
|
215
347
|
organizationId: context.organizationId,
|
|
216
|
-
})
|
|
348
|
+
}, { cache: options.cache })
|
|
217
349
|
|
|
218
350
|
// Check rule count limit
|
|
219
351
|
if (rules.length > MAX_RULES_PER_EXECUTION) {
|
|
@@ -468,14 +600,14 @@ export async function executeSingleRule(
|
|
|
468
600
|
*/
|
|
469
601
|
export async function findApplicableRules(
|
|
470
602
|
em: EntityManager,
|
|
471
|
-
options: RuleDiscoveryOptions
|
|
603
|
+
options: RuleDiscoveryOptions,
|
|
604
|
+
cacheOptions: RuleDiscoveryCacheOptions = {},
|
|
472
605
|
): Promise<BusinessRule[]> {
|
|
473
606
|
// Validate input
|
|
474
607
|
ruleDiscoveryOptionsSchema.parse(options)
|
|
475
608
|
|
|
476
609
|
const { entityType, eventType, tenantId, organizationId, ruleType } = options
|
|
477
|
-
|
|
478
|
-
const where: Partial<BusinessRule> = {
|
|
610
|
+
const baseWhere: Record<string, unknown> = {
|
|
479
611
|
entityType,
|
|
480
612
|
tenantId,
|
|
481
613
|
organizationId,
|
|
@@ -484,16 +616,38 @@ export async function findApplicableRules(
|
|
|
484
616
|
}
|
|
485
617
|
|
|
486
618
|
if (eventType) {
|
|
487
|
-
|
|
619
|
+
baseWhere.eventType = eventType
|
|
488
620
|
}
|
|
489
621
|
|
|
490
622
|
if (ruleType) {
|
|
491
|
-
|
|
623
|
+
baseWhere.ruleType = ruleType
|
|
492
624
|
}
|
|
493
625
|
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
626
|
+
const cached = await getCachedRuleDiscovery(cacheOptions.cache, options)
|
|
627
|
+
let rules: BusinessRule[]
|
|
628
|
+
|
|
629
|
+
if (cached) {
|
|
630
|
+
if (cached.ruleIds.length === 0) {
|
|
631
|
+
rules = []
|
|
632
|
+
} else {
|
|
633
|
+
const cachedRules = await em.find(BusinessRule, {
|
|
634
|
+
...baseWhere,
|
|
635
|
+
id: { $in: cached.ruleIds },
|
|
636
|
+
}, {
|
|
637
|
+
orderBy: { priority: 'DESC', ruleId: 'ASC' },
|
|
638
|
+
} as any)
|
|
639
|
+
const byId = new Map(cachedRules.map((rule) => [rule.id, rule]))
|
|
640
|
+
rules = cached.ruleIds
|
|
641
|
+
.map((id) => byId.get(id))
|
|
642
|
+
.filter((rule): rule is BusinessRule => Boolean(rule))
|
|
643
|
+
}
|
|
644
|
+
} else {
|
|
645
|
+
rules = await em.find(BusinessRule, baseWhere as Partial<BusinessRule>, {
|
|
646
|
+
orderBy: { priority: 'DESC', ruleId: 'ASC' },
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
await cacheRuleDiscovery(cacheOptions.cache, options, rules)
|
|
650
|
+
}
|
|
497
651
|
|
|
498
652
|
// Filter by effective date range
|
|
499
653
|
const now = new Date()
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { EntityManager } from '@mikro-orm/core'
|
|
14
|
-
import { executeRules } from '../lib/rule-engine'
|
|
14
|
+
import { executeRules, resolveBusinessRuleDiscoveryCache } from '../lib/rule-engine'
|
|
15
15
|
|
|
16
16
|
export const metadata = {
|
|
17
17
|
event: '*',
|
|
@@ -64,6 +64,7 @@ export default async function handle(
|
|
|
64
64
|
if (!tenantId || !organizationId) return
|
|
65
65
|
|
|
66
66
|
const em = ctx.resolve<EntityManager>('em')
|
|
67
|
+
const cache = resolveBusinessRuleDiscoveryCache(ctx.resolve)
|
|
67
68
|
|
|
68
69
|
try {
|
|
69
70
|
await executeRules(em, {
|
|
@@ -72,7 +73,7 @@ export default async function handle(
|
|
|
72
73
|
data,
|
|
73
74
|
tenantId,
|
|
74
75
|
organizationId,
|
|
75
|
-
})
|
|
76
|
+
}, { cache })
|
|
76
77
|
} catch (error) {
|
|
77
78
|
console.error(`[business_rules] Rule execution failed for event ${eventName}:`, error)
|
|
78
79
|
}
|
|
@@ -628,9 +628,13 @@ async function decorateProductsAfterList(
|
|
|
628
628
|
"catalogPricingService",
|
|
629
629
|
);
|
|
630
630
|
|
|
631
|
+
const pricingEntries: Array<{ rows: PriceRow[]; context: PricingContext } | null> = [];
|
|
631
632
|
for (const item of items) {
|
|
632
633
|
const id = typeof item.id === "string" ? item.id : null;
|
|
633
|
-
if (!id)
|
|
634
|
+
if (!id) {
|
|
635
|
+
pricingEntries.push(null);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
634
638
|
const offerEntries = offersByProduct.get(id) ?? [];
|
|
635
639
|
item.offers = offerEntries;
|
|
636
640
|
const channelIds = Array.from(
|
|
@@ -668,10 +672,25 @@ async function decorateProductsAfterList(
|
|
|
668
672
|
pricingContext.channelId || channelIds.length !== 1
|
|
669
673
|
? pricingContext
|
|
670
674
|
: { ...pricingContext, channelId: channelIds[0] };
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
quantity: normalizedQuantityForPricing,
|
|
675
|
+
pricingEntries.push({
|
|
676
|
+
rows: priceCandidates,
|
|
677
|
+
context: { ...channelScopedContext, quantity: normalizedQuantityForPricing },
|
|
674
678
|
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const resolveInputs: Array<{ rows: PriceRow[]; context: PricingContext }> = [];
|
|
682
|
+
const resolveIndices: number[] = [];
|
|
683
|
+
for (let i = 0; i < pricingEntries.length; i++) {
|
|
684
|
+
if (pricingEntries[i] !== null) {
|
|
685
|
+
resolveInputs.push(pricingEntries[i]!);
|
|
686
|
+
resolveIndices.push(i);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const priceResults = await pricingService.resolvePriceMany(resolveInputs);
|
|
690
|
+
|
|
691
|
+
for (let i = 0; i < resolveIndices.length; i++) {
|
|
692
|
+
const item = items[resolveIndices[i]];
|
|
693
|
+
const best = priceResults[i];
|
|
675
694
|
if (best) {
|
|
676
695
|
item.pricing = {
|
|
677
696
|
kind: resolvePriceKindCode(best),
|
|
@@ -180,3 +180,12 @@ export async function resolveCatalogPrice(
|
|
|
180
180
|
|
|
181
181
|
return resolved ?? null
|
|
182
182
|
}
|
|
183
|
+
|
|
184
|
+
export async function resolveCatalogPriceBatch(
|
|
185
|
+
entries: Array<{ rows: PriceRow[]; context: PricingContext }>,
|
|
186
|
+
options?: { eventBus?: EventBus | null }
|
|
187
|
+
): Promise<Array<PriceRow | null>> {
|
|
188
|
+
return Promise.all(
|
|
189
|
+
entries.map(({ rows, context }) => resolveCatalogPrice(rows, context, options))
|
|
190
|
+
)
|
|
191
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { EventBus } from '@open-mercato/events'
|
|
2
2
|
import {
|
|
3
3
|
resolveCatalogPrice,
|
|
4
|
+
resolveCatalogPriceBatch,
|
|
4
5
|
type PriceRow,
|
|
5
6
|
type PricingContext,
|
|
6
7
|
} from '../lib/pricing'
|
|
7
8
|
|
|
8
9
|
export interface CatalogPricingService {
|
|
9
10
|
resolvePrice(rows: PriceRow[], context: PricingContext): Promise<PriceRow | null>
|
|
11
|
+
resolvePriceMany(entries: Array<{ rows: PriceRow[]; context: PricingContext }>): Promise<Array<PriceRow | null>>
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export class DefaultCatalogPricingService implements CatalogPricingService {
|
|
@@ -15,6 +17,10 @@ export class DefaultCatalogPricingService implements CatalogPricingService {
|
|
|
15
17
|
async resolvePrice(rows: PriceRow[], context: PricingContext): Promise<PriceRow | null> {
|
|
16
18
|
return resolveCatalogPrice(rows, context, { eventBus: this.eventBus })
|
|
17
19
|
}
|
|
20
|
+
|
|
21
|
+
async resolvePriceMany(entries: Array<{ rows: PriceRow[]; context: PricingContext }>): Promise<Array<PriceRow | null>> {
|
|
22
|
+
return resolveCatalogPriceBatch(entries, { eventBus: this.eventBus })
|
|
23
|
+
}
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
export type { PriceRow, PricingContext }
|
|
@@ -173,6 +173,12 @@ export async function GET(req: Request): Promise<Response> {
|
|
|
173
173
|
}
|
|
174
174
|
portalConnections.add(connection)
|
|
175
175
|
|
|
176
|
+
// Flush an initial comment so the runtime sends the response headers and
|
|
177
|
+
// first body byte immediately, firing the browser EventSource `open`
|
|
178
|
+
// event without waiting for the first heartbeat (30s) or matching event.
|
|
179
|
+
// Comment lines (`:` prefix) are ignored by EventSource message parsing.
|
|
180
|
+
controller.enqueue(encoder.encode(': connected\n\n'))
|
|
181
|
+
|
|
176
182
|
heartbeatTimer = setInterval(() => {
|
|
177
183
|
try {
|
|
178
184
|
controller.enqueue(encoder.encode(':heartbeat\n\n'))
|
|
@@ -185,7 +185,7 @@ function paginateActivityItems(
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
async function decorateActivityItems(
|
|
188
|
+
export async function decorateActivityItems(
|
|
189
189
|
em: EntityManager,
|
|
190
190
|
items: ActivityItem[],
|
|
191
191
|
decryptionScope?: { tenantId: string; organizationId: string },
|
|
@@ -207,12 +207,23 @@ async function decorateActivityItems(
|
|
|
207
207
|
),
|
|
208
208
|
)
|
|
209
209
|
|
|
210
|
+
if (dealIds.length > 0 && (!decryptionScope?.tenantId || !decryptionScope?.organizationId)) {
|
|
211
|
+
const { translate } = await resolveTranslations()
|
|
212
|
+
throw new CrudHttpError(400, {
|
|
213
|
+
error: translate('customers.errors.tenant_required', 'Tenant context is required'),
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
210
217
|
const [users, deals] = await Promise.all([
|
|
211
218
|
authorIds.length > 0 ? em.find(User, { id: { $in: authorIds } }) : Promise.resolve([]),
|
|
212
|
-
dealIds.length > 0
|
|
213
|
-
?
|
|
214
|
-
|
|
215
|
-
|
|
219
|
+
dealIds.length > 0 && decryptionScope
|
|
220
|
+
? findWithDecryption(
|
|
221
|
+
em,
|
|
222
|
+
CustomerDeal,
|
|
223
|
+
{ id: { $in: dealIds }, tenantId: decryptionScope.tenantId, organizationId: decryptionScope.organizationId },
|
|
224
|
+
undefined,
|
|
225
|
+
decryptionScope,
|
|
226
|
+
)
|
|
216
227
|
: Promise.resolve([]),
|
|
217
228
|
])
|
|
218
229
|
|
|
@@ -139,13 +139,23 @@ const crud = makeCrudRoute({
|
|
|
139
139
|
),
|
|
140
140
|
)
|
|
141
141
|
if (!dealIds.length) return
|
|
142
|
+
const tenantId = ctx.auth?.tenantId ?? null
|
|
143
|
+
const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
|
|
144
|
+
if (!tenantId || !organizationId) {
|
|
145
|
+
const { translate } = await resolveTranslations()
|
|
146
|
+
throw new CrudHttpError(400, {
|
|
147
|
+
error: translate('customers.errors.tenant_required', 'Tenant context is required'),
|
|
148
|
+
})
|
|
149
|
+
}
|
|
142
150
|
try {
|
|
143
151
|
const em = (ctx.container.resolve('em') as EntityManager)
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
152
|
+
const deals = await findWithDecryption(
|
|
153
|
+
em,
|
|
154
|
+
CustomerDeal,
|
|
155
|
+
{ id: { $in: dealIds }, tenantId, organizationId },
|
|
156
|
+
undefined,
|
|
157
|
+
{ tenantId, organizationId },
|
|
158
|
+
)
|
|
149
159
|
const map = new Map<string, string>()
|
|
150
160
|
deals.forEach((deal: CustomerDeal) => {
|
|
151
161
|
if (deal.id) map.set(deal.id, deal.title ?? '')
|
|
@@ -8,6 +8,7 @@ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/d
|
|
|
8
8
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
9
9
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
10
10
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
11
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
11
12
|
import {
|
|
12
13
|
CustomerEntity,
|
|
13
14
|
CustomerPersonCompanyLink,
|
|
@@ -115,10 +116,7 @@ export async function GET(req: Request, ctx: { params?: { id?: string } }) {
|
|
|
115
116
|
throw new CrudHttpError(404, { error: translate('customers.errors.company_not_found', 'Company not found') })
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
|
|
119
|
-
if (scope?.filterIds?.length) scope.filterIds.forEach((entry) => allowedOrgIds.add(entry))
|
|
120
|
-
else if (auth.orgId) allowedOrgIds.add(auth.orgId)
|
|
121
|
-
if (allowedOrgIds.size > 0 && company.organizationId && !allowedOrgIds.has(company.organizationId)) {
|
|
119
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: company.organizationId })) {
|
|
122
120
|
throw new CrudHttpError(403, { error: translate('customers.errors.access_denied', 'Access denied') })
|
|
123
121
|
}
|
|
124
122
|
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
withActiveCustomerPersonCompanyLinkFilter,
|
|
48
48
|
} from '../../../lib/personCompanyLinkTable'
|
|
49
49
|
import { normalizeCustomerDetailCustomFields } from '../../detailCustomFields'
|
|
50
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
50
51
|
|
|
51
52
|
export const metadata = {
|
|
52
53
|
GET: { requireAuth: true, requireFeatures: ['customers.companies.view'] },
|
|
@@ -386,11 +387,7 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
|
|
|
386
387
|
if (!company) return notFound('Company not found')
|
|
387
388
|
|
|
388
389
|
if (auth.tenantId && company.tenantId !== auth.tenantId) return notFound('Company not found')
|
|
389
|
-
|
|
390
|
-
if (scope?.filterIds?.length) scope.filterIds.forEach((id) => allowedOrgIds.add(id))
|
|
391
|
-
else if (auth.orgId) allowedOrgIds.add(auth.orgId)
|
|
392
|
-
|
|
393
|
-
if (allowedOrgIds.size && company.organizationId && !allowedOrgIds.has(company.organizationId)) {
|
|
390
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: company.organizationId })) {
|
|
394
391
|
return forbidden('Access denied')
|
|
395
392
|
}
|
|
396
393
|
|
|
@@ -8,6 +8,7 @@ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/d
|
|
|
8
8
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
9
9
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
10
10
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
11
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
11
12
|
import { CustomerCompanyProfile, CustomerDeal, CustomerDealCompanyLink, CustomerEntity } from '../../../../data/entities'
|
|
12
13
|
|
|
13
14
|
const paramsSchema = z.object({
|
|
@@ -97,10 +98,7 @@ export async function GET(req: Request, ctx: { params?: { id?: string } }) {
|
|
|
97
98
|
throw new CrudHttpError(404, { error: translate('customers.errors.deal_not_found', 'Deal not found') })
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
if (scope?.filterIds?.length) scope.filterIds.forEach((entry) => allowedOrgIds.add(entry))
|
|
102
|
-
else if (auth.orgId) allowedOrgIds.add(auth.orgId)
|
|
103
|
-
if (allowedOrgIds.size > 0 && deal.organizationId && !allowedOrgIds.has(deal.organizationId)) {
|
|
101
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: deal.organizationId })) {
|
|
104
102
|
throw new CrudHttpError(403, { error: translate('customers.errors.access_denied', 'Access denied') })
|
|
105
103
|
}
|
|
106
104
|
|
|
@@ -8,6 +8,7 @@ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/d
|
|
|
8
8
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
9
9
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
10
10
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
11
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
11
12
|
import { CustomerDeal, CustomerDealPersonLink, CustomerEntity } from '../../../../data/entities'
|
|
12
13
|
|
|
13
14
|
const paramsSchema = z.object({
|
|
@@ -97,10 +98,7 @@ export async function GET(req: Request, ctx: { params?: { id?: string } }) {
|
|
|
97
98
|
throw new CrudHttpError(404, { error: translate('customers.errors.deal_not_found', 'Deal not found') })
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
if (scope?.filterIds?.length) scope.filterIds.forEach((entry) => allowedOrgIds.add(entry))
|
|
102
|
-
else if (auth.orgId) allowedOrgIds.add(auth.orgId)
|
|
103
|
-
if (allowedOrgIds.size > 0 && deal.organizationId && !allowedOrgIds.has(deal.organizationId)) {
|
|
101
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: deal.organizationId })) {
|
|
104
102
|
throw new CrudHttpError(403, { error: translate('customers.errors.access_denied', 'Access denied') })
|
|
105
103
|
}
|
|
106
104
|
|
|
@@ -22,6 +22,7 @@ import { E } from '#generated/entities.ids.generated'
|
|
|
22
22
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
23
23
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
24
24
|
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
25
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
25
26
|
import { decryptEntitiesWithFallbackScope } from '@open-mercato/shared/lib/encryption/subscriber'
|
|
26
27
|
import { isMissingDealStageTransitionTable, warnMissingDealStageTransitionTable } from '../../../lib/dealStageTransitionTable'
|
|
27
28
|
|
|
@@ -377,15 +378,7 @@ export async function GET(request: Request, context: { params?: Record<string, u
|
|
|
377
378
|
return notFound('Deal not found')
|
|
378
379
|
}
|
|
379
380
|
|
|
380
|
-
|
|
381
|
-
if (Array.isArray(scope?.filterIds)) {
|
|
382
|
-
scope.filterIds.forEach((id) => {
|
|
383
|
-
if (typeof id === 'string' && id.trim().length) allowedOrgIds.add(id)
|
|
384
|
-
})
|
|
385
|
-
} else if (auth.orgId) {
|
|
386
|
-
allowedOrgIds.add(auth.orgId)
|
|
387
|
-
}
|
|
388
|
-
if (allowedOrgIds.size && deal.organizationId && !allowedOrgIds.has(deal.organizationId)) {
|
|
381
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: deal.organizationId })) {
|
|
389
382
|
return forbidden('Access denied')
|
|
390
383
|
}
|
|
391
384
|
|
|
@@ -10,6 +10,7 @@ import { DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/en
|
|
|
10
10
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
11
11
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
12
12
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
13
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
13
14
|
|
|
14
15
|
export const metadata = {
|
|
15
16
|
GET: { requireAuth: true, requireFeatures: ['customers.deals.view'] },
|
|
@@ -97,15 +98,7 @@ export async function GET(request: Request, context: { params?: Record<string, u
|
|
|
97
98
|
return notFound(translate('customers.errors.deal_not_found', 'Deal not found'))
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
if (Array.isArray(scope?.filterIds)) {
|
|
102
|
-
scope.filterIds.forEach((id) => {
|
|
103
|
-
if (typeof id === 'string' && id.trim().length) allowedOrgIds.add(id)
|
|
104
|
-
})
|
|
105
|
-
} else if (auth.orgId) {
|
|
106
|
-
allowedOrgIds.add(auth.orgId)
|
|
107
|
-
}
|
|
108
|
-
if (allowedOrgIds.size && deal.organizationId && !allowedOrgIds.has(deal.organizationId)) {
|
|
101
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: deal.organizationId })) {
|
|
109
102
|
return forbidden(translate('customers.errors.access_denied', 'Access denied'))
|
|
110
103
|
}
|
|
111
104
|
|
|
@@ -7,6 +7,7 @@ import { validateCrudMutationGuard, runCrudMutationGuardAfterSuccess } from '@op
|
|
|
7
7
|
import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
8
8
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
9
9
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
10
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
10
11
|
import { User } from '@open-mercato/core/modules/auth/data/entities'
|
|
11
12
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
12
13
|
import { CustomerEntity, CustomerEntityRole } from '../data/entities'
|
|
@@ -69,24 +70,13 @@ async function buildContext(request: Request) {
|
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
function collectAllowedOrganizationIds(
|
|
73
|
-
scope: Awaited<ReturnType<typeof resolveCustomersRequestContext>>['scope'],
|
|
74
|
-
auth: Awaited<ReturnType<typeof resolveCustomersRequestContext>>['auth'],
|
|
75
|
-
) {
|
|
76
|
-
const allowedOrgIds = new Set<string>()
|
|
77
|
-
if (scope?.filterIds?.length) scope.filterIds.forEach((id) => allowedOrgIds.add(id))
|
|
78
|
-
else if (auth.orgId) allowedOrgIds.add(auth.orgId)
|
|
79
|
-
return allowedOrgIds
|
|
80
|
-
}
|
|
81
|
-
|
|
82
73
|
function ensureRouteOrganizationAccess(
|
|
83
74
|
organizationId: string,
|
|
84
75
|
scope: Awaited<ReturnType<typeof resolveCustomersRequestContext>>['scope'],
|
|
85
76
|
auth: Awaited<ReturnType<typeof resolveCustomersRequestContext>>['auth'],
|
|
86
77
|
translate: Translator,
|
|
87
78
|
) {
|
|
88
|
-
|
|
89
|
-
if (allowedOrgIds.size > 0 && !allowedOrgIds.has(organizationId)) {
|
|
79
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId })) {
|
|
90
80
|
throw new CrudHttpError(403, { error: translate('customers.errors.access_denied', 'Access denied') })
|
|
91
81
|
}
|
|
92
82
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
CustomerPersonProfile,
|
|
8
8
|
} from '@open-mercato/core/modules/customers/data/entities'
|
|
9
9
|
import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
10
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
10
11
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
11
12
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
12
13
|
|
|
@@ -37,11 +38,7 @@ export async function loadPersonContext(req: Request, personId: string) {
|
|
|
37
38
|
throw new CrudHttpError(404, { error: translate('customers.errors.person_not_found', 'Person not found') })
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
if (scope?.filterIds?.length) scope.filterIds.forEach((entry) => allowedOrgIds.add(entry))
|
|
42
|
-
else if (authenticatedAuth.orgId) allowedOrgIds.add(authenticatedAuth.orgId)
|
|
43
|
-
|
|
44
|
-
if (allowedOrgIds.size > 0 && !allowedOrgIds.has(person.organizationId)) {
|
|
41
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth: authenticatedAuth, organizationId: person.organizationId })) {
|
|
45
42
|
throw new CrudHttpError(403, { error: translate('customers.errors.access_denied', 'Access denied') })
|
|
46
43
|
}
|
|
47
44
|
|
|
@@ -8,6 +8,7 @@ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/d
|
|
|
8
8
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
9
9
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
10
10
|
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
11
|
+
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
11
12
|
import {
|
|
12
13
|
CustomerEntity,
|
|
13
14
|
CustomerCompanyProfile,
|
|
@@ -173,11 +174,7 @@ export async function GET(req: Request, ctx: { params?: { id?: string } }) {
|
|
|
173
174
|
throw new CrudHttpError(404, { error: translate('customers.errors.person_not_found', 'Person not found') })
|
|
174
175
|
}
|
|
175
176
|
|
|
176
|
-
|
|
177
|
-
if (scope?.filterIds?.length) scope.filterIds.forEach((entry) => allowedOrgIds.add(entry))
|
|
178
|
-
else if (auth.orgId) allowedOrgIds.add(auth.orgId)
|
|
179
|
-
|
|
180
|
-
if (allowedOrgIds.size > 0 && !allowedOrgIds.has(person.organizationId)) {
|
|
177
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: person.organizationId })) {
|
|
181
178
|
throw new CrudHttpError(403, { error: translate('customers.errors.access_denied', 'Access denied') })
|
|
182
179
|
}
|
|
183
180
|
|