@open-mercato/core 0.6.7-develop.6696.1.4236709f7e → 0.6.7-develop.6725.1.07a748cfc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/api/admin/nav.js +14 -4
- package/dist/modules/auth/api/admin/nav.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +8 -19
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/frontend/login.js +4 -1
- package/dist/modules/auth/frontend/login.js.map +2 -2
- package/dist/modules/auth/lib/backendChrome.js +31 -14
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +3 -4
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +3 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
- package/dist/modules/customers/api/utils.js +12 -18
- package/dist/modules/customers/api/utils.js.map +2 -2
- package/dist/modules/customers/cli.js +11 -4
- package/dist/modules/customers/cli.js.map +2 -2
- package/dist/modules/customers/components/detail/ActiveDealCard.js +2 -1
- package/dist/modules/customers/components/detail/ActiveDealCard.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyKpiBar.js +6 -5
- package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +4 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/dashboard/helpers.js +4 -3
- package/dist/modules/customers/components/detail/dashboard/helpers.js.map +2 -2
- package/dist/modules/customers/components/formConfig.js +14 -10
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/lib/dealStatus.js +38 -0
- package/dist/modules/customers/lib/dealStatus.js.map +7 -0
- package/dist/modules/dashboards/api/widgets/data/batch/route.js +4 -2
- package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/data/route.js +20 -2
- package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
- package/dist/modules/dashboards/lib/aggregations.js +76 -40
- package/dist/modules/dashboards/lib/aggregations.js.map +2 -2
- package/dist/modules/dashboards/lib/exactDecimal.js +69 -0
- package/dist/modules/dashboards/lib/exactDecimal.js.map +7 -0
- package/dist/modules/dashboards/services/widgetDataService.js +228 -5
- package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
- package/dist/modules/feature_toggles/components/FeatureGuard.js +3 -2
- package/dist/modules/feature_toggles/components/FeatureGuard.js.map +2 -2
- package/dist/modules/feature_toggles/components/hooks/staleTime.js +5 -0
- package/dist/modules/feature_toggles/components/hooks/staleTime.js.map +7 -0
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.js +5 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.js.map +2 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagJson.js +5 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagJson.js.map +2 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagNumber.js +5 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagNumber.js.map +2 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagString.js +5 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagString.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/route.js +16 -1
- package/dist/modules/inbox_ops/api/proposals/route.js.map +2 -2
- package/dist/modules/inbox_ops/subscribers/search-reindex-proposal-created.js +32 -0
- package/dist/modules/inbox_ops/subscribers/search-reindex-proposal-created.js.map +7 -0
- package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js +0 -5
- package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js.map +2 -2
- package/dist/modules/integrations/widgets/injection/external-ids/widget.js +14 -0
- package/dist/modules/integrations/widgets/injection/external-ids/widget.js.map +7 -0
- package/dist/modules/messages/lib/searchLookup.js +8 -16
- package/dist/modules/messages/lib/searchLookup.js.map +2 -2
- package/dist/modules/payment_gateways/api/sessions/route.js +5 -0
- package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
- package/dist/modules/payment_gateways/di.js +17 -1
- package/dist/modules/payment_gateways/di.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +8 -0
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/order-amount-reconciliation.js +58 -0
- package/dist/modules/payment_gateways/lib/order-amount-reconciliation.js.map +7 -0
- package/dist/modules/query_index/api/openapi.js +13 -1
- package/dist/modules/query_index/api/openapi.js.map +2 -2
- package/dist/modules/query_index/api/status.js +165 -105
- package/dist/modules/query_index/api/status.js.map +2 -2
- package/dist/modules/query_index/components/QueryIndexesTable.js +128 -41
- package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
- package/dist/modules/query_index/lib/batch.js +11 -4
- package/dist/modules/query_index/lib/batch.js.map +2 -2
- package/dist/modules/query_index/lib/document.js +12 -5
- package/dist/modules/query_index/lib/document.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +36 -4
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/lib/indexer.js +1 -1
- package/dist/modules/query_index/lib/indexer.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +7 -4
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/sales/api/documents/factory.js +14 -3
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +22 -1
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/useSalesChannelsEnabled.js +2 -35
- package/dist/modules/sales/components/useSalesChannelsEnabled.js.map +2 -2
- package/dist/modules/sales/data/validators.js +25 -4
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/di.js +7 -0
- package/dist/modules/sales/di.js.map +2 -2
- package/dist/modules/sales/services/paymentOrderTotalResolver.js +52 -0
- package/dist/modules/sales/services/paymentOrderTotalResolver.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/auth/api/admin/nav.ts +24 -4
- package/src/modules/auth/api/users/route.ts +8 -28
- package/src/modules/auth/frontend/login.tsx +4 -1
- package/src/modules/auth/lib/backendChrome.tsx +51 -13
- package/src/modules/customers/api/companies/[id]/route.ts +3 -4
- package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +3 -2
- package/src/modules/customers/api/utils.ts +12 -31
- package/src/modules/customers/cli.ts +11 -4
- package/src/modules/customers/components/detail/ActiveDealCard.tsx +2 -1
- package/src/modules/customers/components/detail/CompanyKpiBar.tsx +6 -5
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +4 -0
- package/src/modules/customers/components/detail/dashboard/helpers.ts +4 -3
- package/src/modules/customers/components/formConfig.tsx +12 -8
- package/src/modules/customers/lib/dealStatus.ts +48 -0
- package/src/modules/dashboards/api/widgets/data/batch/route.ts +4 -0
- package/src/modules/dashboards/api/widgets/data/route.ts +21 -0
- package/src/modules/dashboards/lib/aggregations.ts +122 -43
- package/src/modules/dashboards/lib/exactDecimal.ts +96 -0
- package/src/modules/dashboards/services/widgetDataService.ts +342 -4
- package/src/modules/feature_toggles/components/FeatureGuard.tsx +9 -2
- package/src/modules/feature_toggles/components/hooks/staleTime.ts +5 -0
- package/src/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.ts +13 -1
- package/src/modules/feature_toggles/components/hooks/useFeatureFlagJson.ts +12 -3
- package/src/modules/feature_toggles/components/hooks/useFeatureFlagNumber.ts +10 -1
- package/src/modules/feature_toggles/components/hooks/useFeatureFlagString.ts +10 -1
- package/src/modules/inbox_ops/api/proposals/route.ts +20 -1
- package/src/modules/inbox_ops/subscribers/search-reindex-proposal-created.ts +39 -0
- package/src/modules/integrations/widgets/injection/external-ids/widget.client.tsx +0 -5
- package/src/modules/integrations/widgets/injection/external-ids/widget.ts +13 -0
- package/src/modules/messages/lib/searchLookup.ts +11 -34
- package/src/modules/payment_gateways/api/sessions/route.ts +5 -0
- package/src/modules/payment_gateways/di.ts +28 -2
- package/src/modules/payment_gateways/i18n/de.json +3 -0
- package/src/modules/payment_gateways/i18n/en.json +3 -0
- package/src/modules/payment_gateways/i18n/es.json +3 -0
- package/src/modules/payment_gateways/i18n/pl.json +3 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +14 -0
- package/src/modules/payment_gateways/lib/order-amount-reconciliation.ts +93 -0
- package/src/modules/query_index/api/openapi.ts +12 -0
- package/src/modules/query_index/api/status.ts +238 -136
- package/src/modules/query_index/components/QueryIndexesTable.tsx +192 -46
- package/src/modules/query_index/i18n/de.json +5 -3
- package/src/modules/query_index/i18n/en.json +5 -3
- package/src/modules/query_index/i18n/es.json +5 -3
- package/src/modules/query_index/i18n/pl.json +5 -3
- package/src/modules/query_index/lib/batch.ts +11 -4
- package/src/modules/query_index/lib/document.ts +35 -4
- package/src/modules/query_index/lib/engine.ts +53 -5
- package/src/modules/query_index/lib/indexer.ts +1 -1
- package/src/modules/query_index/lib/search-tokens.ts +13 -4
- package/src/modules/sales/api/documents/factory.ts +28 -3
- package/src/modules/sales/commands/documents.ts +26 -2
- package/src/modules/sales/components/useSalesChannelsEnabled.ts +2 -48
- package/src/modules/sales/data/validators.ts +50 -5
- package/src/modules/sales/di.ts +9 -0
- package/src/modules/sales/services/paymentOrderTotalResolver.ts +67 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'
|
|
2
|
+
import ExternalIdsWidget from './widget.client'
|
|
3
|
+
|
|
4
|
+
const widget: InjectionWidgetModule = {
|
|
5
|
+
metadata: {
|
|
6
|
+
id: 'integrations.injection.external-ids',
|
|
7
|
+
title: 'External IDs',
|
|
8
|
+
features: ['integrations.view'],
|
|
9
|
+
},
|
|
10
|
+
Widget: ExternalIdsWidget,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default widget
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'
|
|
2
|
+
import type { Kysely } from 'kysely'
|
|
3
|
+
import { findEntityIdsBySearchTokensCompat, type SearchTokenDatabase } from '@open-mercato/shared/lib/search/tokenLookup'
|
|
5
4
|
|
|
6
|
-
function getDb(em: EntityManager): Kysely<
|
|
7
|
-
return em.getKysely<
|
|
5
|
+
function getDb(em: EntityManager): Kysely<SearchTokenDatabase> {
|
|
6
|
+
return em.getKysely<SearchTokenDatabase>()
|
|
8
7
|
}
|
|
9
8
|
|
|
10
9
|
export async function findMessageIdsBySearchTokens({
|
|
@@ -20,33 +19,11 @@ export async function findMessageIdsBySearchTokens({
|
|
|
20
19
|
organizationId: string | null
|
|
21
20
|
fields?: string[]
|
|
22
21
|
}): Promise<string[] | null> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
let searchQuery = db
|
|
31
|
-
.selectFrom('search_tokens')
|
|
32
|
-
.select('entity_id')
|
|
33
|
-
.where('entity_type', '=', 'messages:message')
|
|
34
|
-
.where('field', 'in', fields)
|
|
35
|
-
.where('token_hash', 'in', tokens.hashes)
|
|
36
|
-
.where(sql<boolean>`tenant_id is not distinct from ${tenantId}`)
|
|
37
|
-
|
|
38
|
-
if (organizationId) {
|
|
39
|
-
searchQuery = searchQuery.where('organization_id', '=', organizationId)
|
|
40
|
-
} else {
|
|
41
|
-
searchQuery = searchQuery.where(sql<boolean>`organization_id is not distinct from ${null}`)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const rows = await searchQuery
|
|
45
|
-
.groupBy('entity_id')
|
|
46
|
-
.having(sql<boolean>`count(distinct token_hash) >= ${tokens.hashes.length}`)
|
|
47
|
-
.execute()
|
|
48
|
-
|
|
49
|
-
return rows
|
|
50
|
-
.map((row: { entity_id?: unknown }) => (typeof row.entity_id === 'string' ? row.entity_id : null))
|
|
51
|
-
.filter((id: string | null): id is string => typeof id === 'string' && id.length > 0)
|
|
22
|
+
return findEntityIdsBySearchTokensCompat({
|
|
23
|
+
db: getDb(em),
|
|
24
|
+
entityType: 'messages:message',
|
|
25
|
+
query,
|
|
26
|
+
fields,
|
|
27
|
+
scope: { tenantId, organizationId },
|
|
28
|
+
})
|
|
52
29
|
}
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
3
3
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
4
|
import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
5
|
+
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
5
6
|
import { createSessionSchema } from '../../data/validators'
|
|
6
7
|
import type { PaymentGatewayService } from '../../lib/gateway-service'
|
|
7
8
|
import { paymentGatewaysTag } from '../openapi'
|
|
@@ -98,6 +99,9 @@ export async function POST(req: Request) {
|
|
|
98
99
|
paymentId: transaction.paymentId,
|
|
99
100
|
}, { status: 201 })
|
|
100
101
|
} catch (err: unknown) {
|
|
102
|
+
if (isCrudHttpError(err)) {
|
|
103
|
+
return NextResponse.json(err.body, { status: err.status })
|
|
104
|
+
}
|
|
101
105
|
const message = err instanceof Error ? err.message : 'Failed to create payment session'
|
|
102
106
|
const status = message.includes('No gateway adapter') ? 422 : 502
|
|
103
107
|
return NextResponse.json({ error: message }, { status })
|
|
@@ -113,6 +117,7 @@ export const openApi = {
|
|
|
113
117
|
tags: [paymentGatewaysTag],
|
|
114
118
|
responses: [
|
|
115
119
|
{ status: 201, description: 'Payment session created' },
|
|
120
|
+
{ status: 409, description: 'Amount or currency does not match the referenced order' },
|
|
116
121
|
{ status: 422, description: 'Invalid payload or unknown provider' },
|
|
117
122
|
{ status: 502, description: 'Gateway provider error' },
|
|
118
123
|
],
|
|
@@ -4,9 +4,11 @@ import type { AppContainer } from '@open-mercato/shared/lib/di/container'
|
|
|
4
4
|
import type { CredentialsService } from '../integrations/lib/credentials-service'
|
|
5
5
|
import type { IntegrationLogService } from '../integrations/lib/log-service'
|
|
6
6
|
import type { IntegrationStateService } from '../integrations/lib/state-service'
|
|
7
|
+
import type { PaymentOrderTotalResolver } from '@open-mercato/shared/modules/payment_gateways/types'
|
|
7
8
|
import { GatewayTransaction, WebhookProcessedEvent } from './data/entities'
|
|
8
9
|
import { createPaymentGatewayDescriptorService } from './lib/descriptor-service'
|
|
9
10
|
import { createPaymentGatewayService } from './lib/gateway-service'
|
|
11
|
+
import { isPaymentOrderTotalResolver } from './lib/order-amount-reconciliation'
|
|
10
12
|
|
|
11
13
|
type Cradle = {
|
|
12
14
|
em: EntityManager
|
|
@@ -15,10 +17,34 @@ type Cradle = {
|
|
|
15
17
|
integrationStateService: IntegrationStateService
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
const ORDER_TOTAL_RESOLVER_NAME = 'paymentOrderTotalResolver'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The order-total resolver is owned by whichever module owns orders (`sales`
|
|
24
|
+
* registers the default one). Its absence is the only supported reason to skip
|
|
25
|
+
* amount reconciliation, so this only tolerates a missing registration: a
|
|
26
|
+
* registered resolver that fails to build or does not satisfy the contract
|
|
27
|
+
* throws instead of silently disabling the check.
|
|
28
|
+
*/
|
|
29
|
+
function resolveOrderTotalResolver(container: AppContainer, cradle: Cradle): PaymentOrderTotalResolver | null {
|
|
30
|
+
if (!container.hasRegistration(ORDER_TOTAL_RESOLVER_NAME)) return null
|
|
31
|
+
const candidate = (cradle as Cradle & { paymentOrderTotalResolver?: unknown })[ORDER_TOTAL_RESOLVER_NAME]
|
|
32
|
+
if (!isPaymentOrderTotalResolver(candidate)) {
|
|
33
|
+
throw new Error(`[internal] ${ORDER_TOTAL_RESOLVER_NAME} does not implement resolveOrderTotal`)
|
|
34
|
+
}
|
|
35
|
+
return candidate
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
export function register(container: AppContainer) {
|
|
19
39
|
container.register({
|
|
20
|
-
paymentGatewayService: asFunction((
|
|
21
|
-
createPaymentGatewayService({
|
|
40
|
+
paymentGatewayService: asFunction((cradle: Cradle) =>
|
|
41
|
+
createPaymentGatewayService({
|
|
42
|
+
em: cradle.em,
|
|
43
|
+
integrationCredentialsService: cradle.integrationCredentialsService,
|
|
44
|
+
integrationLogService: cradle.integrationLogService,
|
|
45
|
+
integrationStateService: cradle.integrationStateService,
|
|
46
|
+
paymentOrderTotalResolver: resolveOrderTotalResolver(container, cradle),
|
|
47
|
+
}),
|
|
22
48
|
).scoped().proxy(),
|
|
23
49
|
paymentGatewayDescriptorService: asFunction(({ integrationCredentialsService, integrationStateService }: Cradle) =>
|
|
24
50
|
createPaymentGatewayDescriptorService({ integrationCredentialsService, integrationStateService }),
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
"payment_gateways.captureMethod.automatic": "Automatisch",
|
|
3
3
|
"payment_gateways.captureMethod.manual": "Manuell",
|
|
4
4
|
"payment_gateways.column.gatewayStatus": "Gateway-Status",
|
|
5
|
+
"payment_gateways.errors.sessionAmountMismatch": "Der Betrag der Zahlungssitzung {amount} stimmt nicht mit dem fälligen Betrag der Bestellung {orderId} überein",
|
|
6
|
+
"payment_gateways.errors.sessionCurrencyMismatch": "Die Währung der Zahlungssitzung {currencyCode} stimmt nicht mit der Währung der Bestellung {orderId} überein",
|
|
7
|
+
"payment_gateways.errors.sessionOrderNotFound": "Bestellung {orderId} wurde im aktuellen Gültigkeitsbereich nicht gefunden",
|
|
5
8
|
"payment_gateways.feature.capture": "Autorisierte Zahlungen erfassen",
|
|
6
9
|
"payment_gateways.feature.manage": "Zahlungssitzungen verwalten",
|
|
7
10
|
"payment_gateways.feature.refund": "Erfasste Zahlungen erstatten",
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
"payment_gateways.captureMethod.automatic": "Automatic",
|
|
3
3
|
"payment_gateways.captureMethod.manual": "Manual",
|
|
4
4
|
"payment_gateways.column.gatewayStatus": "Gateway status",
|
|
5
|
+
"payment_gateways.errors.sessionAmountMismatch": "Payment session amount {amount} does not match the amount due for order {orderId}",
|
|
6
|
+
"payment_gateways.errors.sessionCurrencyMismatch": "Payment session currency {currencyCode} does not match the currency of order {orderId}",
|
|
7
|
+
"payment_gateways.errors.sessionOrderNotFound": "Order {orderId} was not found in the current scope",
|
|
5
8
|
"payment_gateways.feature.capture": "Capture authorized payments",
|
|
6
9
|
"payment_gateways.feature.manage": "Manage payment sessions",
|
|
7
10
|
"payment_gateways.feature.refund": "Refund captured payments",
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
"payment_gateways.captureMethod.automatic": "Automatica",
|
|
3
3
|
"payment_gateways.captureMethod.manual": "Manual",
|
|
4
4
|
"payment_gateways.column.gatewayStatus": "Estado de la pasarela",
|
|
5
|
+
"payment_gateways.errors.sessionAmountMismatch": "El importe de la sesión de pago {amount} no coincide con el importe pendiente del pedido {orderId}",
|
|
6
|
+
"payment_gateways.errors.sessionCurrencyMismatch": "La moneda de la sesión de pago {currencyCode} no coincide con la moneda del pedido {orderId}",
|
|
7
|
+
"payment_gateways.errors.sessionOrderNotFound": "No se encontró el pedido {orderId} en el ámbito actual",
|
|
5
8
|
"payment_gateways.feature.capture": "Capturar pagos autorizados",
|
|
6
9
|
"payment_gateways.feature.manage": "Gestionar sesiones de pago",
|
|
7
10
|
"payment_gateways.feature.refund": "Reembolsar pagos capturados",
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
"payment_gateways.captureMethod.automatic": "Automatyczny",
|
|
3
3
|
"payment_gateways.captureMethod.manual": "Manualny",
|
|
4
4
|
"payment_gateways.column.gatewayStatus": "Status bramki",
|
|
5
|
+
"payment_gateways.errors.sessionAmountMismatch": "Kwota sesji płatności {amount} nie zgadza się z kwotą do zapłaty dla zamówienia {orderId}",
|
|
6
|
+
"payment_gateways.errors.sessionCurrencyMismatch": "Waluta sesji płatności {currencyCode} nie zgadza się z walutą zamówienia {orderId}",
|
|
7
|
+
"payment_gateways.errors.sessionOrderNotFound": "Nie znaleziono zamówienia {orderId} w bieżącym zakresie",
|
|
5
8
|
"payment_gateways.feature.capture": "Przechwytywanie autoryzowanych płatności",
|
|
6
9
|
"payment_gateways.feature.manage": "Zarządzanie sesjami płatności",
|
|
7
10
|
"payment_gateways.feature.refund": "Zwroty przechwyconych płatności",
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type GatewayAdapter,
|
|
12
12
|
type GatewayPaymentStatus,
|
|
13
13
|
type PaymentGatewayPresentationRequest,
|
|
14
|
+
type PaymentOrderTotalResolver,
|
|
14
15
|
type UnifiedPaymentStatus,
|
|
15
16
|
} from '@open-mercato/shared/modules/payment_gateways/types'
|
|
16
17
|
import type { CredentialsService } from '../../integrations/lib/credentials-service'
|
|
@@ -21,6 +22,7 @@ import { GatewayPaymentOperation, GatewaySessionInitialization, GatewayTransacti
|
|
|
21
22
|
import { canApplyManualAction, isValidTransition, type ManualGatewayAction } from './status-machine'
|
|
22
23
|
import { emitPaymentGatewayEvent } from '../events'
|
|
23
24
|
import { readGatewayMetadata, readWebhookLog } from './transaction-fields'
|
|
25
|
+
import { reconcileSessionAmountWithOrder } from './order-amount-reconciliation'
|
|
24
26
|
import {
|
|
25
27
|
alignCapturedAmountWithStatus,
|
|
26
28
|
assertCaptureWithinRemaining,
|
|
@@ -76,6 +78,11 @@ export interface PaymentGatewayServiceDeps {
|
|
|
76
78
|
integrationCredentialsService: CredentialsService
|
|
77
79
|
integrationStateService?: IntegrationStateService
|
|
78
80
|
integrationLogService?: IntegrationLogService
|
|
81
|
+
/**
|
|
82
|
+
* Optional seam supplied by the module that owns orders (`sales` by default).
|
|
83
|
+
* When absent, session amounts cannot be reconciled and are accepted as-is.
|
|
84
|
+
*/
|
|
85
|
+
paymentOrderTotalResolver?: PaymentOrderTotalResolver | null
|
|
79
86
|
sessionClaimOptions?: {
|
|
80
87
|
staleAfterMs?: number
|
|
81
88
|
heartbeatIntervalMs?: number
|
|
@@ -405,6 +412,13 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
|
|
|
405
412
|
return {
|
|
406
413
|
async createPaymentSession(input: CreatePaymentSessionInput): Promise<{ transaction: GatewayTransaction; session: CreateSessionResult }> {
|
|
407
414
|
const scope = { organizationId: input.organizationId, tenantId: input.tenantId }
|
|
415
|
+
await reconcileSessionAmountWithOrder({
|
|
416
|
+
orderId: input.orderId,
|
|
417
|
+
amount: input.amount,
|
|
418
|
+
currencyCode: input.currencyCode,
|
|
419
|
+
scope,
|
|
420
|
+
resolver: deps.paymentOrderTotalResolver,
|
|
421
|
+
})
|
|
408
422
|
const { adapter, credentials } = await resolveAdapterAndCredentials(input.providerKey, scope)
|
|
409
423
|
|
|
410
424
|
const sessionInput: CreateSessionInput = {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { conflict } from '@open-mercato/shared/lib/crud/errors'
|
|
2
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
3
|
+
import { createFallbackTranslator, type TranslateWithFallbackFn } from '@open-mercato/shared/lib/i18n/translate'
|
|
4
|
+
import type {
|
|
5
|
+
PaymentGatewayScope,
|
|
6
|
+
PaymentOrderTotal,
|
|
7
|
+
PaymentOrderTotalResolver,
|
|
8
|
+
} from '@open-mercato/shared/modules/payment_gateways/types'
|
|
9
|
+
|
|
10
|
+
/** Amounts are stored as numeric(18,4); anything below that is rounding noise, not a mismatch. */
|
|
11
|
+
const AMOUNT_TOLERANCE = 0.0001
|
|
12
|
+
|
|
13
|
+
function normalizeCurrencyCode(currencyCode: string): string {
|
|
14
|
+
return currencyCode.trim().toUpperCase()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Conflict copy is served from the module catalog on a request path. Contexts
|
|
19
|
+
* without a registered module dictionary (CLI commands, workers, unit tests)
|
|
20
|
+
* fall back to the English template shipped with each call, so a missing
|
|
21
|
+
* dictionary degrades the wording of a rejection but never its outcome.
|
|
22
|
+
*/
|
|
23
|
+
async function resolveConflictTranslator(): Promise<TranslateWithFallbackFn> {
|
|
24
|
+
try {
|
|
25
|
+
const { translate } = await resolveTranslations()
|
|
26
|
+
return translate
|
|
27
|
+
} catch {
|
|
28
|
+
return createFallbackTranslator({})
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isPaymentOrderTotalResolver(candidate: unknown): candidate is PaymentOrderTotalResolver {
|
|
33
|
+
return !!candidate
|
|
34
|
+
&& typeof candidate === 'object'
|
|
35
|
+
&& typeof (candidate as PaymentOrderTotalResolver).resolveOrderTotal === 'function'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function assertSessionAmountMatchesOrderTotal(
|
|
39
|
+
requested: { orderId: string; amount: number; currencyCode: string },
|
|
40
|
+
orderTotal: PaymentOrderTotal,
|
|
41
|
+
translate: TranslateWithFallbackFn,
|
|
42
|
+
): void {
|
|
43
|
+
if (normalizeCurrencyCode(requested.currencyCode) !== normalizeCurrencyCode(orderTotal.currencyCode)) {
|
|
44
|
+
throw conflict(translate(
|
|
45
|
+
'payment_gateways.errors.sessionCurrencyMismatch',
|
|
46
|
+
'Payment session currency {currencyCode} does not match the currency of order {orderId}',
|
|
47
|
+
{ currencyCode: normalizeCurrencyCode(requested.currencyCode), orderId: requested.orderId },
|
|
48
|
+
))
|
|
49
|
+
}
|
|
50
|
+
if (Math.abs(requested.amount - orderTotal.amountDue) > AMOUNT_TOLERANCE) {
|
|
51
|
+
throw conflict(translate(
|
|
52
|
+
'payment_gateways.errors.sessionAmountMismatch',
|
|
53
|
+
'Payment session amount {amount} does not match the amount due for order {orderId}',
|
|
54
|
+
{ amount: requested.amount, orderId: requested.orderId },
|
|
55
|
+
))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Reconciles a caller-supplied session amount against the authoritative amount
|
|
61
|
+
* due for the referenced order. Reconciliation is skipped when the request
|
|
62
|
+
* references no order, or when no module registered a resolver (for example an
|
|
63
|
+
* installation without the `sales` module) — there is nothing authoritative to
|
|
64
|
+
* compare against in either case. A referenced order that does not resolve
|
|
65
|
+
* inside the caller's tenant/organization scope is rejected exactly like an
|
|
66
|
+
* unknown one, so the response never reveals whether it exists elsewhere.
|
|
67
|
+
*/
|
|
68
|
+
export async function reconcileSessionAmountWithOrder(input: {
|
|
69
|
+
orderId?: string
|
|
70
|
+
amount: number
|
|
71
|
+
currencyCode: string
|
|
72
|
+
scope: PaymentGatewayScope
|
|
73
|
+
resolver?: PaymentOrderTotalResolver | null
|
|
74
|
+
}): Promise<void> {
|
|
75
|
+
const { orderId, resolver } = input
|
|
76
|
+
if (!orderId || !resolver) return
|
|
77
|
+
|
|
78
|
+
const orderTotal = await resolver.resolveOrderTotal(orderId, input.scope)
|
|
79
|
+
const translate = await resolveConflictTranslator()
|
|
80
|
+
if (!orderTotal) {
|
|
81
|
+
throw conflict(translate(
|
|
82
|
+
'payment_gateways.errors.sessionOrderNotFound',
|
|
83
|
+
'Order {orderId} was not found in the current scope',
|
|
84
|
+
{ orderId },
|
|
85
|
+
))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
assertSessionAmountMatchesOrderTotal(
|
|
89
|
+
{ orderId, amount: input.amount, currencyCode: input.currencyCode },
|
|
90
|
+
orderTotal,
|
|
91
|
+
translate,
|
|
92
|
+
)
|
|
93
|
+
}
|
|
@@ -37,13 +37,25 @@ export const queryIndexJobSchema = z.object({
|
|
|
37
37
|
|
|
38
38
|
export const queryIndexStatusItemSchema = z.object({
|
|
39
39
|
entityId: z.string(),
|
|
40
|
+
/** Human label. Code-defined entities have no label registry, so this mirrors `entityId`. */
|
|
40
41
|
label: z.string(),
|
|
41
42
|
baseCount: z.number().int().nonnegative().nullable(),
|
|
42
43
|
indexCount: z.number().int().nonnegative().nullable(),
|
|
43
44
|
vectorCount: z.number().int().nonnegative().nullable().optional(),
|
|
45
|
+
/** The entity declares `buildSource`, i.e. it is configured for vector search. */
|
|
44
46
|
vectorEnabled: z.boolean().optional(),
|
|
47
|
+
/** Additive: vector indexing can actually run (auto-indexing on, provider reachable, tenant opted in). */
|
|
48
|
+
vectorIndexingActive: z.boolean().optional(),
|
|
49
|
+
fulltextCount: z.number().int().nonnegative().nullable().optional(),
|
|
50
|
+
fulltextEnabled: z.boolean().optional(),
|
|
51
|
+
/** Additive: the entity has active custom field definitions in the current scope. */
|
|
52
|
+
hasCustomFields: z.boolean().optional(),
|
|
53
|
+
/** Aggregate health: query index in sync AND, when vector-configured, vector coverage complete. */
|
|
45
54
|
ok: z.boolean(),
|
|
55
|
+
/** Additive: query index alone is in sync with the base table, independent of vector/fulltext. */
|
|
56
|
+
queryIndexOk: z.boolean().optional(),
|
|
46
57
|
job: queryIndexJobSchema,
|
|
58
|
+
refreshedAt: z.string().datetime().nullable().optional(),
|
|
47
59
|
})
|
|
48
60
|
|
|
49
61
|
export const queryIndexErrorLogSchema = z.object({
|