@open-mercato/core 0.6.6-develop.6370.1.efeac59f8b → 0.6.6-develop.6374.1.49e692558f
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/feature_toggles/components/formConfig.js +4 -4
- package/dist/modules/feature_toggles/components/formConfig.js.map +2 -2
- package/dist/modules/feature_toggles/components/overrideFormConfig.js +4 -4
- package/dist/modules/feature_toggles/components/overrideFormConfig.js.map +2 -2
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js +12 -0
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js.map +2 -2
- package/dist/modules/messages/components/useMessagesInboxBulkActions.js +14 -1
- package/dist/modules/messages/components/useMessagesInboxBulkActions.js.map +2 -2
- package/dist/modules/payment_gateways/api/transactions/[id]/route.js +3 -2
- package/dist/modules/payment_gateways/api/transactions/[id]/route.js.map +2 -2
- package/dist/modules/payment_gateways/encryption.js +16 -0
- package/dist/modules/payment_gateways/encryption.js.map +7 -0
- package/dist/modules/payment_gateways/lib/gateway-service.js +6 -5
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/transaction-fields.js +17 -0
- package/dist/modules/payment_gateways/lib/transaction-fields.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/feature_toggles/components/formConfig.tsx +2 -2
- package/src/modules/feature_toggles/components/overrideFormConfig.tsx +2 -2
- package/src/modules/inbox_ops/components/proposals/ActionCard.tsx +23 -0
- package/src/modules/messages/components/useMessagesInboxBulkActions.ts +15 -2
- package/src/modules/messages/i18n/de.json +1 -0
- package/src/modules/messages/i18n/en.json +1 -0
- package/src/modules/messages/i18n/es.json +1 -0
- package/src/modules/messages/i18n/pl.json +1 -0
- package/src/modules/payment_gateways/api/transactions/[id]/route.ts +3 -2
- package/src/modules/payment_gateways/encryption.ts +20 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +6 -5
- package/src/modules/payment_gateways/lib/transaction-fields.ts +24 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "@open-mercato/ui/primitives/select";
|
|
12
12
|
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
13
13
|
import { booleanOverrideSelectValue } from "./overrideFormConfig.js";
|
|
14
|
-
function
|
|
14
|
+
function DefaultValueField(props) {
|
|
15
15
|
const t = useT();
|
|
16
16
|
const selectedType = props.values?.type;
|
|
17
17
|
switch (selectedType) {
|
|
@@ -121,7 +121,7 @@ function createFieldDefinitions(t) {
|
|
|
121
121
|
id: "defaultValue",
|
|
122
122
|
label: "",
|
|
123
123
|
type: "custom",
|
|
124
|
-
component:
|
|
124
|
+
component: (props) => /* @__PURE__ */ jsx(DefaultValueField, { ...props }),
|
|
125
125
|
description: t("feature_toggles.form.fields.defaultValue.description")
|
|
126
126
|
}
|
|
127
127
|
];
|
|
@@ -154,8 +154,8 @@ function createFormGroups(t) {
|
|
|
154
154
|
];
|
|
155
155
|
}
|
|
156
156
|
export {
|
|
157
|
+
DefaultValueField,
|
|
157
158
|
createFieldDefinitions,
|
|
158
|
-
createFormGroups
|
|
159
|
-
renderDefaultValueCreateComponent
|
|
159
|
+
createFormGroups
|
|
160
160
|
};
|
|
161
161
|
//# sourceMappingURL=formConfig.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/feature_toggles/components/formConfig.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { CrudFormGroup, CrudCustomFieldRenderProps, CrudField } from \"@open-mercato/ui/backend/CrudForm\";\nimport { JsonBuilder } from \"@open-mercato/ui/backend/JsonBuilder\";\nimport { Input } from \"@open-mercato/ui/primitives/input\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@open-mercato/ui/primitives/select\";\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { booleanOverrideSelectValue } from \"./overrideFormConfig\";\n\n\nexport function
|
|
5
|
-
"mappings": ";AAuBoB,cASI,YATJ;AArBpB,SAAS,mBAAmB;AAC5B,SAAS,aAAa;AACtB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,YAAY;AACrB,SAAS,kCAAkC;AAGpC,SAAS,
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { CrudFormGroup, CrudCustomFieldRenderProps, CrudField } from \"@open-mercato/ui/backend/CrudForm\";\nimport { JsonBuilder } from \"@open-mercato/ui/backend/JsonBuilder\";\nimport { Input } from \"@open-mercato/ui/primitives/input\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@open-mercato/ui/primitives/select\";\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { booleanOverrideSelectValue } from \"./overrideFormConfig\";\n\n\nexport function DefaultValueField(props: CrudCustomFieldRenderProps) {\n const t = useT()\n const selectedType = props.values?.type as string;\n\n switch (selectedType) {\n case 'boolean':\n return (\n <div>\n <label className=\"block text-sm font-medium mb-2\">{t('feature_toggles.form.fields.defaultValue.boolean.label', 'Default Value (Boolean)')}</label>\n <Select\n value={booleanOverrideSelectValue(props.value)}\n onValueChange={(value) => props.setValue(value === 'true')}\n disabled={props.disabled}\n >\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"true\">{t('feature_toggles.values.true', 'True')}</SelectItem>\n <SelectItem value=\"false\">{t('feature_toggles.values.false', 'False')}</SelectItem>\n </SelectContent>\n </Select>\n </div>\n );\n\n case 'string':\n return (\n <div>\n <label className=\"block text-sm font-medium mb-2\">{t('feature_toggles.form.fields.defaultValue.string.label', 'Default Value (String)')}</label>\n <Input\n type=\"text\"\n value={props.value as string || ''}\n onChange={(e) => props.setValue(e.target.value)}\n placeholder={t('feature_toggles.form.fields.defaultValue.string.placeholder', 'Enter default string value')}\n disabled={props.disabled}\n autoFocus={props.autoFocus}\n />\n </div>\n );\n\n case 'number':\n return (\n <div>\n <label className=\"block text-sm font-medium mb-2\">{t('feature_toggles.form.fields.defaultValue.number.label', 'Default Value (Number)')}</label>\n <Input\n type=\"number\"\n value={props.value as number || 0}\n onChange={(e) => props.setValue(Number(e.target.value) || 0)}\n disabled={props.disabled}\n autoFocus={props.autoFocus}\n />\n </div>\n );\n\n case 'json':\n return (\n <div>\n <label className=\"block text-sm font-medium mb-2\">{t('feature_toggles.form.fields.defaultValue.json.label', 'Default Value (JSON)')}</label>\n <JsonBuilder\n value={props.value}\n onChange={props.setValue}\n disabled={props.disabled}\n />\n </div>\n );\n\n default:\n return (\n <div className=\"text-sm text-muted-foreground p-4 text-center bg-muted/30 rounded border border-dashed\">\n {t('feature_toggles.form.fields.defaultValue.selectType', 'Please select a type above to configure the default value')}\n </div>\n );\n }\n}\n\nexport function createFieldDefinitions(\n t: (key: string) => string,\n): CrudField[] {\n return [\n {\n id: 'identifier',\n label: t('feature_toggles.form.fields.identifier.label'),\n type: 'text',\n required: true,\n },\n {\n id: 'name',\n label: t('feature_toggles.form.fields.name.label'),\n type: 'text',\n required: true,\n },\n {\n id: 'description',\n label: t('feature_toggles.form.fields.description.label'),\n type: 'textarea',\n required: false,\n },\n {\n id: 'category',\n label: t('feature_toggles.form.fields.category.label'),\n type: 'text',\n required: false,\n },\n {\n id: 'type',\n label: t('feature_toggles.form.fields.type.label'),\n type: 'select',\n required: true,\n options: [\n { label: t('feature_toggles.types.boolean'), value: 'boolean' },\n { label: t('feature_toggles.types.string'), value: 'string' },\n { label: t('feature_toggles.types.number'), value: 'number' },\n { label: t('feature_toggles.types.json'), value: 'json' },\n ],\n },\n {\n id: 'defaultValue',\n label: '',\n type: 'custom',\n component: (props) => <DefaultValueField {...props} />,\n description: t('feature_toggles.form.fields.defaultValue.description'),\n },\n ]\n}\n\nexport function createFormGroups(\n t: (key: string) => string,\n): CrudFormGroup[] {\n return [\n {\n id: 'basic',\n title: t('feature_toggles.form.groups.basic'),\n column: 1,\n fields: [\n 'identifier',\n 'name',\n 'description',\n 'category',\n ],\n },\n {\n id: 'type',\n title: t('feature_toggles.form.groups.type'),\n column: 1,\n fields: ['type'],\n },\n {\n id: 'defaultValue',\n title: t('feature_toggles.form.groups.defaultValue'),\n column: 1,\n fields: ['defaultValue'],\n },\n ]\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -13,7 +13,7 @@ import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
|
13
13
|
function booleanOverrideSelectValue(value) {
|
|
14
14
|
return value === true || value === "true" ? "true" : "false";
|
|
15
15
|
}
|
|
16
|
-
function
|
|
16
|
+
function OverrideValueField(props) {
|
|
17
17
|
const t = useT();
|
|
18
18
|
const toggleType = props.values?.toggleType;
|
|
19
19
|
const isOverride = props.values?.isOverride;
|
|
@@ -98,7 +98,7 @@ function createOverrideFieldDefinitions(t) {
|
|
|
98
98
|
id: "overrideValue",
|
|
99
99
|
label: "",
|
|
100
100
|
type: "custom",
|
|
101
|
-
component:
|
|
101
|
+
component: (props) => /* @__PURE__ */ jsx(OverrideValueField, { ...props }),
|
|
102
102
|
description: t("feature_toggles.override.fields.overrideValue.description")
|
|
103
103
|
}
|
|
104
104
|
];
|
|
@@ -120,9 +120,9 @@ function createOverrideFormGroups(t) {
|
|
|
120
120
|
];
|
|
121
121
|
}
|
|
122
122
|
export {
|
|
123
|
+
OverrideValueField,
|
|
123
124
|
booleanOverrideSelectValue,
|
|
124
125
|
createOverrideFieldDefinitions,
|
|
125
|
-
createOverrideFormGroups
|
|
126
|
-
renderOverrideValueComponent
|
|
126
|
+
createOverrideFormGroups
|
|
127
127
|
};
|
|
128
128
|
//# sourceMappingURL=overrideFormConfig.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/feature_toggles/components/overrideFormConfig.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { CrudFormGroup, CrudCustomFieldRenderProps, CrudField } from \"@open-mercato/ui/backend/CrudForm\";\nimport { JsonBuilder } from \"@open-mercato/ui/backend/JsonBuilder\";\nimport { Input } from \"@open-mercato/ui/primitives/input\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@open-mercato/ui/primitives/select\";\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\n/**\n * Normalize a boolean override value (which may be stored as a real boolean or a\n * 'true'/'false' string) to the string the `<Select>` needs. Passing a raw boolean\n * to `Select.value` matched no `<SelectItem>` and rendered blank (#2410).\n */\nexport function booleanOverrideSelectValue(value: unknown): 'true' | 'false' {\n return value === true || value === 'true' ? 'true' : 'false'\n}\n\nexport function
|
|
5
|
-
"mappings": ";AA6BY,cAmBY,YAnBZ;AA3BZ,SAAS,mBAAmB;AAC5B,SAAS,aAAa;AACtB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,YAAY;AAOd,SAAS,2BAA2B,OAAkC;AAC3E,SAAO,UAAU,QAAQ,UAAU,SAAS,SAAS;AACvD;AAEO,SAAS,
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { CrudFormGroup, CrudCustomFieldRenderProps, CrudField } from \"@open-mercato/ui/backend/CrudForm\";\nimport { JsonBuilder } from \"@open-mercato/ui/backend/JsonBuilder\";\nimport { Input } from \"@open-mercato/ui/primitives/input\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@open-mercato/ui/primitives/select\";\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\n/**\n * Normalize a boolean override value (which may be stored as a real boolean or a\n * 'true'/'false' string) to the string the `<Select>` needs. Passing a raw boolean\n * to `Select.value` matched no `<SelectItem>` and rendered blank (#2410).\n */\nexport function booleanOverrideSelectValue(value: unknown): 'true' | 'false' {\n return value === true || value === 'true' ? 'true' : 'false'\n}\n\nexport function OverrideValueField(props: CrudCustomFieldRenderProps) {\n const t = useT()\n const toggleType = props.values?.toggleType as string;\n const isOverride = props.values?.isOverride as boolean;\n\n if (!isOverride) {\n return (\n <div className=\"text-sm text-muted-foreground p-4 text-center bg-muted/30 rounded border border-dashed\">\n {t('feature_toggles.override.disabled', 'Override is disabled. Values will be inherited from the default configuration.')}\n </div>\n );\n }\n\n switch (toggleType) {\n case 'boolean':\n return (\n <div>\n <label className=\"block text-sm font-medium mb-2\">{t('feature_toggles.override.fields.value.boolean.label', 'Override Value (Boolean)')}</label>\n <Select\n value={booleanOverrideSelectValue(props.value)}\n onValueChange={(value) => props.setValue(value === 'true')}\n disabled={props.disabled}\n >\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"true\">{t('feature_toggles.values.true', 'True')}</SelectItem>\n <SelectItem value=\"false\">{t('feature_toggles.values.false', 'False')}</SelectItem>\n </SelectContent>\n </Select>\n </div>\n );\n\n case 'string':\n return (\n <div>\n <label className=\"block text-sm font-medium mb-2\">{t('feature_toggles.override.fields.value.string.label', 'Override Value (String)')}</label>\n <Input\n type=\"text\"\n value={props.value as string || ''}\n onChange={(e) => props.setValue(e.target.value)}\n placeholder={t('feature_toggles.override.fields.value.string.placeholder', 'Enter override string value')}\n disabled={props.disabled}\n autoFocus={props.autoFocus}\n />\n </div>\n );\n\n case 'number':\n return (\n <div>\n <label className=\"block text-sm font-medium mb-2\">{t('feature_toggles.override.fields.value.number.label', 'Override Value (Number)')}</label>\n <Input\n type=\"number\"\n value={props.value as number || 0}\n onChange={(e) => props.setValue(Number(e.target.value) || 0)}\n disabled={props.disabled}\n autoFocus={props.autoFocus}\n />\n </div>\n );\n\n case 'json':\n return (\n <div>\n <label className=\"block text-sm font-medium mb-2\">{t('feature_toggles.override.fields.value.json.label', 'Override Value (JSON)')}</label>\n <JsonBuilder\n value={props.value}\n onChange={props.setValue}\n disabled={props.disabled}\n />\n </div>\n );\n\n default:\n return (\n <div className=\"text-sm text-muted-foreground p-4 text-center bg-muted/30 rounded border border-dashed\">\n {t('feature_toggles.override.unknownType', 'Unknown toggle type. Cannot configure override value.')}\n </div>\n );\n }\n}\n\nexport function createOverrideFieldDefinitions(\n t: (key: string) => string,\n): CrudField[] {\n return [\n {\n id: 'isOverride',\n label: t('feature_toggles.override.fields.isOverride.label'),\n type: 'checkbox',\n required: false,\n description: t('feature_toggles.override.fields.isOverride.description'),\n },\n {\n id: 'overrideValue',\n label: '',\n type: 'custom',\n component: (props) => <OverrideValueField {...props} />,\n description: t('feature_toggles.override.fields.overrideValue.description'),\n },\n ]\n}\n\nexport function createOverrideFormGroups(\n t: (key: string) => string,\n): CrudFormGroup[] {\n return [\n {\n id: 'overrideMode',\n title: t('feature_toggles.override.groups.mode'),\n column: 1,\n fields: ['isOverride'],\n },\n {\n id: 'overrideValue',\n title: t('feature_toggles.override.groups.value'),\n column: 1,\n fields: ['overrideValue'],\n },\n ]\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -283,6 +283,18 @@ function ActionCard({
|
|
|
283
283
|
] })
|
|
284
284
|
] });
|
|
285
285
|
}
|
|
286
|
+
if (action.status !== "pending") {
|
|
287
|
+
const isProcessing = action.status === "processing";
|
|
288
|
+
const statusLabel = action.status === "accepted" ? t("inbox_ops.status.accepted", "Accepted") : isProcessing ? t("inbox_ops.status.processing", "Processing") : action.status;
|
|
289
|
+
return /* @__PURE__ */ jsxs("div", { className: "border rounded-lg p-3 md:p-4 bg-muted/50", children: [
|
|
290
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-2", children: [
|
|
291
|
+
isProcessing ? /* @__PURE__ */ jsx(RefreshCw, { className: "h-5 w-5 text-muted-foreground flex-shrink-0 animate-spin" }) : /* @__PURE__ */ jsx(CheckCircle, { className: "h-5 w-5 text-muted-foreground flex-shrink-0" }),
|
|
292
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: label }),
|
|
293
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: statusLabel })
|
|
294
|
+
] }),
|
|
295
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: displayDescription })
|
|
296
|
+
] });
|
|
297
|
+
}
|
|
286
298
|
const hasNameIssue = hasContactNameIssue(action);
|
|
287
299
|
return /* @__PURE__ */ jsxs("div", { className: "border rounded-lg p-3 md:p-4", children: [
|
|
288
300
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-2", children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/inbox_ops/components/proposals/ActionCard.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n CheckCircle,\n XCircle,\n Pencil,\n AlertTriangle,\n RefreshCw,\n Package,\n FileText,\n MessageSquare,\n Truck,\n UserPlus,\n Link2,\n Activity,\n ShoppingBag,\n} from 'lucide-react'\nimport type { ActionDetail, DiscrepancyDetail } from './types'\nimport { hasContactNameIssue } from '../../lib/contactValidation'\n\nexport { hasContactNameIssue }\n\n/**\n * Resolves discrepancy description i18n keys stored in the database.\n * Falls back to the raw description string for legacy data or LLM-generated descriptions.\n */\nexport function useDiscrepancyDescriptions(): (description: string, foundValue?: string | null) => string {\n const t = useT()\n const translations: Record<string, string> = {\n 'inbox_ops.discrepancy.desc.no_channel': t('inbox_ops.discrepancy.desc.no_channel', 'No sales channel available. Create a channel in Sales settings before accepting this order.'),\n 'inbox_ops.discrepancy.desc.no_currency': t('inbox_ops.discrepancy.desc.no_currency', 'No currency could be resolved for this order. Set a currency code or configure a sales channel with a default currency.'),\n 'inbox_ops.discrepancy.desc.product_not_matched': t('inbox_ops.discrepancy.desc.product_not_matched', 'Product could not be matched to any catalog product'),\n 'inbox_ops.discrepancy.desc.no_matching_contact': t('inbox_ops.discrepancy.desc.no_matching_contact', 'No matching contact found'),\n 'inbox_ops.discrepancy.desc.draft_reply_no_contact': t('inbox_ops.discrepancy.desc.draft_reply_no_contact', 'Draft reply target has no matching contact. Create the contact first.'),\n 'inbox_ops.discrepancy.desc.duplicate_order_reference': t('inbox_ops.discrepancy.desc.duplicate_order_reference', 'An order with this customer reference already exists'),\n }\n return (description: string, foundValue?: string | null) => {\n const translated = translations[description]\n if (!translated) return description\n if (foundValue && (description === 'inbox_ops.discrepancy.desc.product_not_matched' || description === 'inbox_ops.discrepancy.desc.no_matching_contact')) {\n return `${translated}: ${foundValue}`\n }\n return translated\n }\n}\n\n/**\n * Resolves action description i18n keys stored in the database.\n * Auto-generated actions store keys like `inbox_ops.action.desc.create_contact`;\n * LLM-generated actions store plain text which is returned as-is.\n */\nexport function useActionDescriptionResolver(): (description: string, payload: Record<string, unknown>) => string {\n const t = useT()\n return (description: string, payload: Record<string, unknown>) => {\n if (!description.startsWith('inbox_ops.action.desc.')) return description\n const name = (payload.name as string) || (payload.contactName as string) || ''\n const email = (payload.email as string) || (payload.emailAddress as string) || ''\n const title = (payload.title as string) || ''\n const toName = (payload.toName as string) || (payload.to as string) || ''\n const subject = (payload.subject as string) || ''\n const translations: Record<string, string> = {\n 'inbox_ops.action.desc.create_contact': t('inbox_ops.action.desc.create_contact', 'Create contact for {name} ({email})')\n .replace('{name}', name).replace('{email}', email),\n 'inbox_ops.action.desc.link_contact': t('inbox_ops.action.desc.link_contact', 'Link {name} ({email}) to existing contact')\n .replace('{name}', name).replace('{email}', email),\n 'inbox_ops.action.desc.create_product': t('inbox_ops.action.desc.create_product', 'Create catalog product \"{title}\"')\n .replace('{title}', title),\n 'inbox_ops.action.desc.draft_reply': t('inbox_ops.action.desc.draft_reply', 'Draft reply to {toName}: {subject}')\n .replace('{toName}', toName).replace('{subject}', subject),\n }\n return translations[description] || description\n }\n}\n\nconst ACTION_TYPE_ICONS: Record<string, React.ElementType> = {\n create_order: Package,\n create_quote: FileText,\n update_order: Package,\n update_shipment: Truck,\n create_contact: UserPlus,\n create_product: ShoppingBag,\n link_contact: Link2,\n log_activity: Activity,\n draft_reply: MessageSquare,\n}\n\nexport function useActionTypeLabels(): Record<string, string> {\n const t = useT()\n return {\n create_order: t('inbox_ops.action_type.create_order', 'Create Sales Order'),\n create_quote: t('inbox_ops.action_type.create_quote', 'Create Quote'),\n update_order: t('inbox_ops.action_type.update_order', 'Update Order'),\n update_shipment: t('inbox_ops.action_type.update_shipment', 'Update Shipment'),\n create_contact: t('inbox_ops.action_type.create_contact', 'Create Contact'),\n create_product: t('inbox_ops.action_type.create_product', 'Create Product'),\n link_contact: t('inbox_ops.action_type.link_contact', 'Link Contact'),\n log_activity: t('inbox_ops.action_type.log_activity', 'Log Activity'),\n draft_reply: t('inbox_ops.action_type.draft_reply', 'Draft Reply'),\n }\n}\n\nexport function ConfidenceBadge({ value }: { value: string }) {\n const num = parseFloat(value)\n const pct = Math.round(num * 100)\n const color = num >= 0.8 ? 'text-status-success-text' : num >= 0.6 ? 'text-status-warning-text' : 'text-status-error-text'\n const bgColor = num >= 0.8 ? 'bg-status-success-icon' : num >= 0.6 ? 'bg-status-warning-icon' : 'bg-status-error-icon'\n const width = Math.round(num * 100)\n return (\n <div className=\"flex items-center gap-2\">\n <span className={`text-sm font-medium ${color}`}>{pct}%</span>\n <div className=\"w-24 h-2 bg-muted rounded-full overflow-hidden\">\n <div className={`h-full ${bgColor} rounded-full`} style={{ width: `${width}%` }} />\n </div>\n </div>\n )\n}\n\nfunction OrderPreview({ payload }: { payload: Record<string, unknown> }) {\n const t = useT()\n const lineItems = (payload.lineItems as Record<string, unknown>[]) || []\n const customerName = (payload.customerName as string) || ''\n const currencyCode = (payload.currencyCode as string) || ''\n const notes = (payload.notes as string) || ''\n const deliveryDate = (payload.requestedDeliveryDate as string) || ''\n\n return (\n <div className=\"mt-2 space-y-2 text-xs\">\n {customerName && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.customer', 'Customer')}:</span>\n <span>{customerName}</span>\n {typeof payload.customerEmail === 'string' && <span className=\"text-muted-foreground\">({payload.customerEmail})</span>}\n </div>\n )}\n {lineItems.length > 0 && (\n <div className=\"border rounded overflow-hidden\">\n <table className=\"w-full text-xs\">\n <thead>\n <tr className=\"bg-muted/50\">\n <th className=\"text-left px-2 py-1 font-medium\">{t('inbox_ops.preview.product', 'Product')}</th>\n <th className=\"text-right px-2 py-1 font-medium\">{t('inbox_ops.preview.qty', 'Qty')}</th>\n <th className=\"text-right px-2 py-1 font-medium\">{t('inbox_ops.preview.price', 'Price')}</th>\n </tr>\n </thead>\n <tbody>\n {lineItems.map((item, index) => (\n <tr key={index} className=\"border-t\">\n <td className=\"px-2 py-1\">{(item.productName as string) || '\u2014'}</td>\n <td className=\"px-2 py-1 text-right\">{String(item.quantity ?? '')}</td>\n <td className=\"px-2 py-1 text-right\">{item.unitPrice ? `${item.unitPrice} ${currencyCode}` : '\u2014'}</td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n {(deliveryDate || notes) && (\n <div className=\"flex flex-wrap gap-3\">\n {deliveryDate && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.delivery', 'Delivery')}:</span>\n <span>{deliveryDate}</span>\n </div>\n )}\n {notes && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.notes', 'Notes')}:</span>\n <span className=\"truncate max-w-[200px]\">{notes}</span>\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n\nfunction ProductPreview({ payload }: { payload: Record<string, unknown> }) {\n const t = useT()\n const title = (payload.title as string) || ''\n const sku = (payload.sku as string) || ''\n const unitPrice = (payload.unitPrice as string) || ''\n const currencyCode = (payload.currencyCode as string) || ''\n const kind = (payload.kind as string) || 'product'\n\n return (\n <div className=\"mt-2 space-y-1 text-xs\">\n {title && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.product_title', 'Title')}:</span>\n <span className=\"font-medium\">{title}</span>\n </div>\n )}\n <div className=\"flex flex-wrap gap-3\">\n {sku && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.sku', 'SKU')}:</span>\n <span>{sku}</span>\n </div>\n )}\n {unitPrice && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.price', 'Price')}:</span>\n <span>{unitPrice}{currencyCode ? ` ${currencyCode}` : ''}</span>\n </div>\n )}\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.edit_dialog.kind', 'Kind')}:</span>\n <span>{kind}</span>\n </div>\n </div>\n </div>\n )\n}\n\nexport function ActionCard({\n action,\n discrepancies,\n actionTypeLabels,\n onAccept,\n onReject,\n onRetry,\n onEdit,\n translatedDescription,\n resolveDiscrepancyDescription,\n}: {\n action: ActionDetail\n discrepancies: DiscrepancyDetail[]\n actionTypeLabels: Record<string, string>\n onAccept: (id: string) => void\n onReject: (id: string) => void\n onRetry: (id: string) => void\n onEdit: (action: ActionDetail) => void\n translatedDescription?: string\n resolveDiscrepancyDescription?: (description: string, foundValue?: string | null) => string\n}) {\n const t = useT()\n const Icon = ACTION_TYPE_ICONS[action.actionType] || Package\n const label = actionTypeLabels[action.actionType] || action.actionType\n const resolveActionDescription = useActionDescriptionResolver()\n\n const actionDiscrepancies = discrepancies.filter((d) => d.actionId === action.id && !d.resolved)\n const hasBlockingDiscrepancies = actionDiscrepancies.some((d) => d.severity === 'error')\n const displayDescription = translatedDescription || resolveActionDescription(action.description, action.payload)\n\n if (action.status === 'executed') {\n return (\n <div className=\"border rounded-lg p-3 md:p-4 bg-status-success-bg border-status-success-border\">\n <div className=\"flex items-center gap-2 mb-2\">\n <CheckCircle className=\"h-5 w-5 text-status-success-icon flex-shrink-0\" />\n <span className=\"text-sm font-medium\">{label}</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">{displayDescription}</p>\n {action.createdEntityId && (\n <div className=\"mt-2\">\n <span className=\"text-xs text-status-success-text\">\n {t('inbox_ops.action.created_entity', 'Created {type}').replace('{type}', action.createdEntityType || '')} \u00B7 {action.executedAt && new Date(action.executedAt).toLocaleString()}\n </span>\n </div>\n )}\n </div>\n )\n }\n\n if (action.status === 'rejected') {\n return (\n <div className=\"border rounded-lg p-3 md:p-4 bg-muted/50 opacity-60\">\n <div className=\"flex items-center gap-2 mb-2\">\n <XCircle className=\"h-5 w-5 text-muted-foreground flex-shrink-0\" />\n <span className=\"text-sm font-medium line-through\">{label}</span>\n <span className=\"text-xs text-muted-foreground\">{t('inbox_ops.status.rejected', 'Rejected')}</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">{displayDescription}</p>\n </div>\n )\n }\n\n if (action.status === 'failed') {\n return (\n <div className=\"border rounded-lg p-3 md:p-4 bg-status-error-bg border-status-error-border\">\n <div className=\"flex items-center gap-2 mb-2\">\n <AlertTriangle className=\"h-5 w-5 text-status-error-icon flex-shrink-0\" />\n <span className=\"text-sm font-medium\">{label}</span>\n <span className=\"text-xs text-status-error-text\">{t('inbox_ops.extraction_failed', 'Failed')}</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">{displayDescription}</p>\n {action.executionError && (\n <p className=\"text-xs text-status-error-text mt-1\">{action.executionError}</p>\n )}\n <div className=\"mt-3 flex items-center gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onRetry(action.id)}\n >\n <RefreshCw className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.retry', 'Retry')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onEdit(action)}\n >\n <Pencil className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.edit', 'Edit')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onReject(action.id)}\n >\n <XCircle className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.reject', 'Reject')}\n </Button>\n </div>\n </div>\n )\n }\n\n const hasNameIssue = hasContactNameIssue(action)\n\n return (\n <div className=\"border rounded-lg p-3 md:p-4\">\n <div className=\"flex items-center gap-2 mb-2\">\n <Icon className=\"h-5 w-5 text-primary flex-shrink-0\" />\n <span className=\"text-sm font-medium\">{label}</span>\n <ConfidenceBadge value={action.confidence} />\n </div>\n <p className=\"text-sm text-foreground/80 mb-2\">{displayDescription}</p>\n\n {(action.actionType === 'create_order' || action.actionType === 'create_quote') && (\n <OrderPreview payload={action.payload} />\n )}\n\n {action.actionType === 'create_product' && (\n <ProductPreview payload={action.payload} />\n )}\n\n {actionDiscrepancies.length > 0 && (\n <div className=\"mb-3 space-y-1\">\n {actionDiscrepancies.map((d) => (\n <div key={d.id} className={`flex items-start gap-2 text-xs rounded px-2 py-1.5 ${\n d.severity === 'error' ? 'bg-status-error-bg text-status-error-text' : 'bg-status-warning-bg text-status-warning-text'\n }`}>\n <AlertTriangle className=\"h-3 w-3 mt-0.5 flex-shrink-0\" />\n <div>\n <span>{resolveDiscrepancyDescription ? resolveDiscrepancyDescription(d.description, d.foundValue) : d.description}</span>\n {(d.expectedValue || d.foundValue) && (\n <div className=\"mt-0.5 text-overline opacity-80\">\n {d.expectedValue && <span>{t('inbox_ops.discrepancy.expected', 'Expected')}: {d.expectedValue}</span>}\n {d.expectedValue && d.foundValue && <span> \u00B7 </span>}\n {d.foundValue && <span>{t('inbox_ops.discrepancy.found', 'Found')}: {d.foundValue}</span>}\n </div>\n )}\n </div>\n </div>\n ))}\n </div>\n )}\n\n {hasNameIssue && (\n <div className=\"mb-3 flex items-start gap-2 text-xs rounded px-2 py-1.5 bg-status-warning-bg text-status-warning-text\">\n <AlertTriangle className=\"h-3 w-3 mt-0.5 flex-shrink-0\" />\n <span>{action.actionType === 'link_contact'\n ? t('inbox_ops.contact.link_name_missing_warning', 'Contact name is missing. Please edit and provide a name before accepting.')\n : t('inbox_ops.contact.name_missing_warning', 'First and last name could not be extracted. Please edit before accepting.')\n }</span>\n </div>\n )}\n\n <div className=\"flex items-center gap-2\">\n <div title={\n hasNameIssue\n ? action.actionType === 'link_contact'\n ? t('inbox_ops.contact.link_name_missing_warning', 'Contact name is missing. Please edit and provide a name before accepting.')\n : t('inbox_ops.contact.name_missing_warning', 'First and last name could not be extracted. Please edit before accepting.')\n : hasBlockingDiscrepancies\n ? t('inbox_ops.action.accept_blocked', 'Resolve errors before accepting')\n : undefined\n }>\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onAccept(action.id)}\n disabled={hasBlockingDiscrepancies || hasNameIssue}\n >\n <CheckCircle className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.accept', 'Accept')}\n </Button>\n </div>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onEdit(action)}\n >\n <Pencil className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.edit', 'Edit')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onReject(action.id)}\n >\n <XCircle className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.reject', 'Reject')}\n </Button>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n CheckCircle,\n XCircle,\n Pencil,\n AlertTriangle,\n RefreshCw,\n Package,\n FileText,\n MessageSquare,\n Truck,\n UserPlus,\n Link2,\n Activity,\n ShoppingBag,\n} from 'lucide-react'\nimport type { ActionDetail, DiscrepancyDetail } from './types'\nimport { hasContactNameIssue } from '../../lib/contactValidation'\n\nexport { hasContactNameIssue }\n\n/**\n * Resolves discrepancy description i18n keys stored in the database.\n * Falls back to the raw description string for legacy data or LLM-generated descriptions.\n */\nexport function useDiscrepancyDescriptions(): (description: string, foundValue?: string | null) => string {\n const t = useT()\n const translations: Record<string, string> = {\n 'inbox_ops.discrepancy.desc.no_channel': t('inbox_ops.discrepancy.desc.no_channel', 'No sales channel available. Create a channel in Sales settings before accepting this order.'),\n 'inbox_ops.discrepancy.desc.no_currency': t('inbox_ops.discrepancy.desc.no_currency', 'No currency could be resolved for this order. Set a currency code or configure a sales channel with a default currency.'),\n 'inbox_ops.discrepancy.desc.product_not_matched': t('inbox_ops.discrepancy.desc.product_not_matched', 'Product could not be matched to any catalog product'),\n 'inbox_ops.discrepancy.desc.no_matching_contact': t('inbox_ops.discrepancy.desc.no_matching_contact', 'No matching contact found'),\n 'inbox_ops.discrepancy.desc.draft_reply_no_contact': t('inbox_ops.discrepancy.desc.draft_reply_no_contact', 'Draft reply target has no matching contact. Create the contact first.'),\n 'inbox_ops.discrepancy.desc.duplicate_order_reference': t('inbox_ops.discrepancy.desc.duplicate_order_reference', 'An order with this customer reference already exists'),\n }\n return (description: string, foundValue?: string | null) => {\n const translated = translations[description]\n if (!translated) return description\n if (foundValue && (description === 'inbox_ops.discrepancy.desc.product_not_matched' || description === 'inbox_ops.discrepancy.desc.no_matching_contact')) {\n return `${translated}: ${foundValue}`\n }\n return translated\n }\n}\n\n/**\n * Resolves action description i18n keys stored in the database.\n * Auto-generated actions store keys like `inbox_ops.action.desc.create_contact`;\n * LLM-generated actions store plain text which is returned as-is.\n */\nexport function useActionDescriptionResolver(): (description: string, payload: Record<string, unknown>) => string {\n const t = useT()\n return (description: string, payload: Record<string, unknown>) => {\n if (!description.startsWith('inbox_ops.action.desc.')) return description\n const name = (payload.name as string) || (payload.contactName as string) || ''\n const email = (payload.email as string) || (payload.emailAddress as string) || ''\n const title = (payload.title as string) || ''\n const toName = (payload.toName as string) || (payload.to as string) || ''\n const subject = (payload.subject as string) || ''\n const translations: Record<string, string> = {\n 'inbox_ops.action.desc.create_contact': t('inbox_ops.action.desc.create_contact', 'Create contact for {name} ({email})')\n .replace('{name}', name).replace('{email}', email),\n 'inbox_ops.action.desc.link_contact': t('inbox_ops.action.desc.link_contact', 'Link {name} ({email}) to existing contact')\n .replace('{name}', name).replace('{email}', email),\n 'inbox_ops.action.desc.create_product': t('inbox_ops.action.desc.create_product', 'Create catalog product \"{title}\"')\n .replace('{title}', title),\n 'inbox_ops.action.desc.draft_reply': t('inbox_ops.action.desc.draft_reply', 'Draft reply to {toName}: {subject}')\n .replace('{toName}', toName).replace('{subject}', subject),\n }\n return translations[description] || description\n }\n}\n\nconst ACTION_TYPE_ICONS: Record<string, React.ElementType> = {\n create_order: Package,\n create_quote: FileText,\n update_order: Package,\n update_shipment: Truck,\n create_contact: UserPlus,\n create_product: ShoppingBag,\n link_contact: Link2,\n log_activity: Activity,\n draft_reply: MessageSquare,\n}\n\nexport function useActionTypeLabels(): Record<string, string> {\n const t = useT()\n return {\n create_order: t('inbox_ops.action_type.create_order', 'Create Sales Order'),\n create_quote: t('inbox_ops.action_type.create_quote', 'Create Quote'),\n update_order: t('inbox_ops.action_type.update_order', 'Update Order'),\n update_shipment: t('inbox_ops.action_type.update_shipment', 'Update Shipment'),\n create_contact: t('inbox_ops.action_type.create_contact', 'Create Contact'),\n create_product: t('inbox_ops.action_type.create_product', 'Create Product'),\n link_contact: t('inbox_ops.action_type.link_contact', 'Link Contact'),\n log_activity: t('inbox_ops.action_type.log_activity', 'Log Activity'),\n draft_reply: t('inbox_ops.action_type.draft_reply', 'Draft Reply'),\n }\n}\n\nexport function ConfidenceBadge({ value }: { value: string }) {\n const num = parseFloat(value)\n const pct = Math.round(num * 100)\n const color = num >= 0.8 ? 'text-status-success-text' : num >= 0.6 ? 'text-status-warning-text' : 'text-status-error-text'\n const bgColor = num >= 0.8 ? 'bg-status-success-icon' : num >= 0.6 ? 'bg-status-warning-icon' : 'bg-status-error-icon'\n const width = Math.round(num * 100)\n return (\n <div className=\"flex items-center gap-2\">\n <span className={`text-sm font-medium ${color}`}>{pct}%</span>\n <div className=\"w-24 h-2 bg-muted rounded-full overflow-hidden\">\n <div className={`h-full ${bgColor} rounded-full`} style={{ width: `${width}%` }} />\n </div>\n </div>\n )\n}\n\nfunction OrderPreview({ payload }: { payload: Record<string, unknown> }) {\n const t = useT()\n const lineItems = (payload.lineItems as Record<string, unknown>[]) || []\n const customerName = (payload.customerName as string) || ''\n const currencyCode = (payload.currencyCode as string) || ''\n const notes = (payload.notes as string) || ''\n const deliveryDate = (payload.requestedDeliveryDate as string) || ''\n\n return (\n <div className=\"mt-2 space-y-2 text-xs\">\n {customerName && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.customer', 'Customer')}:</span>\n <span>{customerName}</span>\n {typeof payload.customerEmail === 'string' && <span className=\"text-muted-foreground\">({payload.customerEmail})</span>}\n </div>\n )}\n {lineItems.length > 0 && (\n <div className=\"border rounded overflow-hidden\">\n <table className=\"w-full text-xs\">\n <thead>\n <tr className=\"bg-muted/50\">\n <th className=\"text-left px-2 py-1 font-medium\">{t('inbox_ops.preview.product', 'Product')}</th>\n <th className=\"text-right px-2 py-1 font-medium\">{t('inbox_ops.preview.qty', 'Qty')}</th>\n <th className=\"text-right px-2 py-1 font-medium\">{t('inbox_ops.preview.price', 'Price')}</th>\n </tr>\n </thead>\n <tbody>\n {lineItems.map((item, index) => (\n <tr key={index} className=\"border-t\">\n <td className=\"px-2 py-1\">{(item.productName as string) || '\u2014'}</td>\n <td className=\"px-2 py-1 text-right\">{String(item.quantity ?? '')}</td>\n <td className=\"px-2 py-1 text-right\">{item.unitPrice ? `${item.unitPrice} ${currencyCode}` : '\u2014'}</td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n {(deliveryDate || notes) && (\n <div className=\"flex flex-wrap gap-3\">\n {deliveryDate && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.delivery', 'Delivery')}:</span>\n <span>{deliveryDate}</span>\n </div>\n )}\n {notes && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.notes', 'Notes')}:</span>\n <span className=\"truncate max-w-[200px]\">{notes}</span>\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n\nfunction ProductPreview({ payload }: { payload: Record<string, unknown> }) {\n const t = useT()\n const title = (payload.title as string) || ''\n const sku = (payload.sku as string) || ''\n const unitPrice = (payload.unitPrice as string) || ''\n const currencyCode = (payload.currencyCode as string) || ''\n const kind = (payload.kind as string) || 'product'\n\n return (\n <div className=\"mt-2 space-y-1 text-xs\">\n {title && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.product_title', 'Title')}:</span>\n <span className=\"font-medium\">{title}</span>\n </div>\n )}\n <div className=\"flex flex-wrap gap-3\">\n {sku && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.sku', 'SKU')}:</span>\n <span>{sku}</span>\n </div>\n )}\n {unitPrice && (\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.preview.price', 'Price')}:</span>\n <span>{unitPrice}{currencyCode ? ` ${currencyCode}` : ''}</span>\n </div>\n )}\n <div className=\"flex gap-1\">\n <span className=\"text-muted-foreground\">{t('inbox_ops.edit_dialog.kind', 'Kind')}:</span>\n <span>{kind}</span>\n </div>\n </div>\n </div>\n )\n}\n\nexport function ActionCard({\n action,\n discrepancies,\n actionTypeLabels,\n onAccept,\n onReject,\n onRetry,\n onEdit,\n translatedDescription,\n resolveDiscrepancyDescription,\n}: {\n action: ActionDetail\n discrepancies: DiscrepancyDetail[]\n actionTypeLabels: Record<string, string>\n onAccept: (id: string) => void\n onReject: (id: string) => void\n onRetry: (id: string) => void\n onEdit: (action: ActionDetail) => void\n translatedDescription?: string\n resolveDiscrepancyDescription?: (description: string, foundValue?: string | null) => string\n}) {\n const t = useT()\n const Icon = ACTION_TYPE_ICONS[action.actionType] || Package\n const label = actionTypeLabels[action.actionType] || action.actionType\n const resolveActionDescription = useActionDescriptionResolver()\n\n const actionDiscrepancies = discrepancies.filter((d) => d.actionId === action.id && !d.resolved)\n const hasBlockingDiscrepancies = actionDiscrepancies.some((d) => d.severity === 'error')\n const displayDescription = translatedDescription || resolveActionDescription(action.description, action.payload)\n\n if (action.status === 'executed') {\n return (\n <div className=\"border rounded-lg p-3 md:p-4 bg-status-success-bg border-status-success-border\">\n <div className=\"flex items-center gap-2 mb-2\">\n <CheckCircle className=\"h-5 w-5 text-status-success-icon flex-shrink-0\" />\n <span className=\"text-sm font-medium\">{label}</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">{displayDescription}</p>\n {action.createdEntityId && (\n <div className=\"mt-2\">\n <span className=\"text-xs text-status-success-text\">\n {t('inbox_ops.action.created_entity', 'Created {type}').replace('{type}', action.createdEntityType || '')} \u00B7 {action.executedAt && new Date(action.executedAt).toLocaleString()}\n </span>\n </div>\n )}\n </div>\n )\n }\n\n if (action.status === 'rejected') {\n return (\n <div className=\"border rounded-lg p-3 md:p-4 bg-muted/50 opacity-60\">\n <div className=\"flex items-center gap-2 mb-2\">\n <XCircle className=\"h-5 w-5 text-muted-foreground flex-shrink-0\" />\n <span className=\"text-sm font-medium line-through\">{label}</span>\n <span className=\"text-xs text-muted-foreground\">{t('inbox_ops.status.rejected', 'Rejected')}</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">{displayDescription}</p>\n </div>\n )\n }\n\n if (action.status === 'failed') {\n return (\n <div className=\"border rounded-lg p-3 md:p-4 bg-status-error-bg border-status-error-border\">\n <div className=\"flex items-center gap-2 mb-2\">\n <AlertTriangle className=\"h-5 w-5 text-status-error-icon flex-shrink-0\" />\n <span className=\"text-sm font-medium\">{label}</span>\n <span className=\"text-xs text-status-error-text\">{t('inbox_ops.extraction_failed', 'Failed')}</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">{displayDescription}</p>\n {action.executionError && (\n <p className=\"text-xs text-status-error-text mt-1\">{action.executionError}</p>\n )}\n <div className=\"mt-3 flex items-center gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onRetry(action.id)}\n >\n <RefreshCw className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.retry', 'Retry')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onEdit(action)}\n >\n <Pencil className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.edit', 'Edit')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onReject(action.id)}\n >\n <XCircle className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.reject', 'Reject')}\n </Button>\n </div>\n </div>\n )\n }\n\n if (action.status !== 'pending') {\n const isProcessing = action.status === 'processing'\n const statusLabel = action.status === 'accepted'\n ? t('inbox_ops.status.accepted', 'Accepted')\n : isProcessing\n ? t('inbox_ops.status.processing', 'Processing')\n : action.status\n return (\n <div className=\"border rounded-lg p-3 md:p-4 bg-muted/50\">\n <div className=\"flex items-center gap-2 mb-2\">\n {isProcessing ? (\n <RefreshCw className=\"h-5 w-5 text-muted-foreground flex-shrink-0 animate-spin\" />\n ) : (\n <CheckCircle className=\"h-5 w-5 text-muted-foreground flex-shrink-0\" />\n )}\n <span className=\"text-sm font-medium\">{label}</span>\n <span className=\"text-xs text-muted-foreground\">{statusLabel}</span>\n </div>\n <p className=\"text-sm text-muted-foreground\">{displayDescription}</p>\n </div>\n )\n }\n\n const hasNameIssue = hasContactNameIssue(action)\n\n return (\n <div className=\"border rounded-lg p-3 md:p-4\">\n <div className=\"flex items-center gap-2 mb-2\">\n <Icon className=\"h-5 w-5 text-primary flex-shrink-0\" />\n <span className=\"text-sm font-medium\">{label}</span>\n <ConfidenceBadge value={action.confidence} />\n </div>\n <p className=\"text-sm text-foreground/80 mb-2\">{displayDescription}</p>\n\n {(action.actionType === 'create_order' || action.actionType === 'create_quote') && (\n <OrderPreview payload={action.payload} />\n )}\n\n {action.actionType === 'create_product' && (\n <ProductPreview payload={action.payload} />\n )}\n\n {actionDiscrepancies.length > 0 && (\n <div className=\"mb-3 space-y-1\">\n {actionDiscrepancies.map((d) => (\n <div key={d.id} className={`flex items-start gap-2 text-xs rounded px-2 py-1.5 ${\n d.severity === 'error' ? 'bg-status-error-bg text-status-error-text' : 'bg-status-warning-bg text-status-warning-text'\n }`}>\n <AlertTriangle className=\"h-3 w-3 mt-0.5 flex-shrink-0\" />\n <div>\n <span>{resolveDiscrepancyDescription ? resolveDiscrepancyDescription(d.description, d.foundValue) : d.description}</span>\n {(d.expectedValue || d.foundValue) && (\n <div className=\"mt-0.5 text-overline opacity-80\">\n {d.expectedValue && <span>{t('inbox_ops.discrepancy.expected', 'Expected')}: {d.expectedValue}</span>}\n {d.expectedValue && d.foundValue && <span> \u00B7 </span>}\n {d.foundValue && <span>{t('inbox_ops.discrepancy.found', 'Found')}: {d.foundValue}</span>}\n </div>\n )}\n </div>\n </div>\n ))}\n </div>\n )}\n\n {hasNameIssue && (\n <div className=\"mb-3 flex items-start gap-2 text-xs rounded px-2 py-1.5 bg-status-warning-bg text-status-warning-text\">\n <AlertTriangle className=\"h-3 w-3 mt-0.5 flex-shrink-0\" />\n <span>{action.actionType === 'link_contact'\n ? t('inbox_ops.contact.link_name_missing_warning', 'Contact name is missing. Please edit and provide a name before accepting.')\n : t('inbox_ops.contact.name_missing_warning', 'First and last name could not be extracted. Please edit before accepting.')\n }</span>\n </div>\n )}\n\n <div className=\"flex items-center gap-2\">\n <div title={\n hasNameIssue\n ? action.actionType === 'link_contact'\n ? t('inbox_ops.contact.link_name_missing_warning', 'Contact name is missing. Please edit and provide a name before accepting.')\n : t('inbox_ops.contact.name_missing_warning', 'First and last name could not be extracted. Please edit before accepting.')\n : hasBlockingDiscrepancies\n ? t('inbox_ops.action.accept_blocked', 'Resolve errors before accepting')\n : undefined\n }>\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onAccept(action.id)}\n disabled={hasBlockingDiscrepancies || hasNameIssue}\n >\n <CheckCircle className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.accept', 'Accept')}\n </Button>\n </div>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onEdit(action)}\n >\n <Pencil className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.edit', 'Edit')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-11 md:h-9\"\n onClick={() => onReject(action.id)}\n >\n <XCircle className=\"h-4 w-4 mr-1\" />\n {t('inbox_ops.action.reject', 'Reject')}\n </Button>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -32,6 +32,14 @@ const MESSAGE_BULK_REQUESTS = {
|
|
|
32
32
|
errorKey: "messages.errors.stateChangeFailed",
|
|
33
33
|
errorFallback: "Failed to update message state."
|
|
34
34
|
},
|
|
35
|
+
unarchive: {
|
|
36
|
+
method: "DELETE",
|
|
37
|
+
buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/archive`,
|
|
38
|
+
successKey: "messages.bulk.flash.unarchiveSuccess",
|
|
39
|
+
successFallback: "{count} messages moved to inbox.",
|
|
40
|
+
errorKey: "messages.errors.stateChangeFailed",
|
|
41
|
+
errorFallback: "Failed to update message state."
|
|
42
|
+
},
|
|
35
43
|
delete: {
|
|
36
44
|
method: "DELETE",
|
|
37
45
|
buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}`,
|
|
@@ -57,6 +65,11 @@ const MESSAGE_BULK_ACTIONS = {
|
|
|
57
65
|
labelKey: "messages.actions.archive",
|
|
58
66
|
labelFallback: "Archive"
|
|
59
67
|
},
|
|
68
|
+
unarchive: {
|
|
69
|
+
id: "messages-unarchive",
|
|
70
|
+
labelKey: "messages.actions.unarchive",
|
|
71
|
+
labelFallback: "Unarchive"
|
|
72
|
+
},
|
|
60
73
|
delete: {
|
|
61
74
|
id: "messages-delete",
|
|
62
75
|
labelKey: "messages.actions.delete",
|
|
@@ -66,7 +79,7 @@ const MESSAGE_BULK_ACTIONS = {
|
|
|
66
79
|
};
|
|
67
80
|
const MESSAGE_BULK_ACTION_IDS_BY_FOLDER = {
|
|
68
81
|
inbox: ["markRead", "markUnread", "archive", "delete"],
|
|
69
|
-
archived: ["markRead", "markUnread", "delete"],
|
|
82
|
+
archived: ["markRead", "markUnread", "unarchive", "delete"],
|
|
70
83
|
sent: ["delete"],
|
|
71
84
|
drafts: ["delete"],
|
|
72
85
|
all: []
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/messages/components/useMessagesInboxBulkActions.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useQueryClient } from '@tanstack/react-query'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { BulkAction } from '@open-mercato/ui/backend/DataTable'\nimport type { FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { toErrorMessage } from './message-detail/utils'\n\nexport type MessageFolder = 'inbox' | 'sent' | 'drafts' | 'archived' | 'all'\n\nexport type MessageBulkActionId = 'markRead' | 'markUnread' | 'archive' | 'delete'\n\ntype BulkExecutionSummary = {\n action: MessageBulkActionId\n total: number\n succeeded: number\n failed: number\n}\n\ntype MessageBulkRequestConfig = {\n method: 'PUT' | 'DELETE'\n buildUrl: (messageId: string) => string\n successKey: string\n successFallback: string\n errorKey: string\n errorFallback: string\n}\n\ntype MessageBulkActionDefinition = {\n id: string\n labelKey: string\n labelFallback: string\n destructive?: boolean\n}\n\ntype MessageInboxBulkMutationContext = {\n actionId: MessageBulkActionId\n messageIds: string[]\n folder: MessageFolder\n page: number\n search: string\n filters: FilterValues\n retryLastMutation: () => Promise<boolean>\n}\n\ntype UseMessagesInboxBulkActionsInput = {\n folder: MessageFolder\n page: number\n search: string\n filterValues: FilterValues\n}\n\ntype MessageInboxBulkRow = {\n id: string\n}\n\nconst MESSAGE_BULK_REQUESTS: Record<MessageBulkActionId, MessageBulkRequestConfig> = {\n markRead: {\n method: 'PUT',\n buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/read`,\n successKey: 'messages.bulk.flash.markReadSuccess',\n successFallback: '{count} messages marked as read.',\n errorKey: 'messages.errors.stateChangeFailed',\n errorFallback: 'Failed to update message state.',\n },\n markUnread: {\n method: 'DELETE',\n buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/read`,\n successKey: 'messages.bulk.flash.markUnreadSuccess',\n successFallback: '{count} messages marked as unread.',\n errorKey: 'messages.errors.stateChangeFailed',\n errorFallback: 'Failed to update message state.',\n },\n archive: {\n method: 'PUT',\n buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/archive`,\n successKey: 'messages.bulk.flash.archiveSuccess',\n successFallback: '{count} messages archived.',\n errorKey: 'messages.errors.stateChangeFailed',\n errorFallback: 'Failed to update message state.',\n },\n delete: {\n method: 'DELETE',\n buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}`,\n successKey: 'messages.bulk.flash.deleteSuccess',\n successFallback: '{count} messages deleted.',\n errorKey: 'messages.errors.deleteFailed',\n errorFallback: 'Failed to delete message.',\n },\n}\n\nconst MESSAGE_BULK_ACTIONS: Record<MessageBulkActionId, MessageBulkActionDefinition> = {\n markRead: {\n id: 'messages-mark-read',\n labelKey: 'messages.actions.markRead',\n labelFallback: 'Mark read',\n },\n markUnread: {\n id: 'messages-mark-unread',\n labelKey: 'messages.actions.markUnread',\n labelFallback: 'Mark unread',\n },\n archive: {\n id: 'messages-archive',\n labelKey: 'messages.actions.archive',\n labelFallback: 'Archive',\n },\n delete: {\n id: 'messages-delete',\n labelKey: 'messages.actions.delete',\n labelFallback: 'Delete',\n destructive: true,\n },\n}\n\nconst MESSAGE_BULK_ACTION_IDS_BY_FOLDER = {\n inbox: ['markRead', 'markUnread', 'archive', 'delete'],\n archived: ['markRead', 'markUnread', 'delete'],\n sent: ['delete'],\n drafts: ['delete'],\n all: [],\n} satisfies Record<MessageFolder, readonly MessageBulkActionId[]>\n\nexport function getMessageBulkActionIdsForFolder(folder: MessageFolder): MessageBulkActionId[] {\n return [...MESSAGE_BULK_ACTION_IDS_BY_FOLDER[folder]]\n}\n\nfunction normalizeSelectionScopeValue(value: unknown): unknown {\n if (value == null) return undefined\n if (typeof value === 'string') {\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n }\n if (Array.isArray(value)) {\n const normalized = value\n .map((item) => normalizeSelectionScopeValue(item))\n .filter((item) => item !== undefined)\n return normalized.length > 0 ? normalized : undefined\n }\n if (typeof value === 'object') {\n const normalizedEntries = Object.entries(value)\n .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))\n .map(([key, nestedValue]) => [key, normalizeSelectionScopeValue(nestedValue)] as const)\n .filter(([, nestedValue]) => nestedValue !== undefined)\n if (normalizedEntries.length === 0) return undefined\n return Object.fromEntries(normalizedEntries)\n }\n return value\n}\n\nfunction buildMessageSelectionScopeKey(\n folder: MessageFolder,\n page: number,\n search: string,\n filterValues: FilterValues,\n): string {\n return JSON.stringify({\n folder,\n page,\n search: search.trim(),\n filters: normalizeSelectionScopeValue(filterValues) ?? {},\n })\n}\n\nasync function runWithConcurrency<TItem>(\n items: TItem[],\n limit: number,\n worker: (item: TItem) => Promise<void>,\n): Promise<PromiseSettledResult<void>[]> {\n if (items.length === 0) return []\n\n const results: PromiseSettledResult<void>[] = new Array(items.length)\n let nextIndex = 0\n\n const runWorker = async () => {\n while (nextIndex < items.length) {\n const currentIndex = nextIndex\n nextIndex += 1\n try {\n await worker(items[currentIndex])\n results[currentIndex] = { status: 'fulfilled', value: undefined }\n } catch (error) {\n results[currentIndex] = { status: 'rejected', reason: error }\n }\n }\n }\n\n await Promise.all(\n Array.from({ length: Math.min(limit, items.length) }, () => runWorker()),\n )\n\n return results\n}\n\nexport function useMessagesInboxBulkActions<T extends MessageInboxBulkRow>({\n folder,\n page,\n search,\n filterValues,\n}: UseMessagesInboxBulkActionsInput): {\n bulkActions: BulkAction<T>[] | undefined\n selectionScopeKey: string\n injectionContext: Record<string, unknown>\n ConfirmDialogElement: React.ReactNode\n} {\n const t = useT()\n const queryClient = useQueryClient()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const { runMutation, retryLastMutation } = useGuardedMutation<MessageInboxBulkMutationContext>({\n contextId: 'messages-inbox-bulk-actions',\n })\n\n const selectionScopeKey = React.useMemo(\n () => buildMessageSelectionScopeKey(folder, page, search, filterValues),\n [filterValues, folder, page, search],\n )\n const injectionContext = React.useMemo<Record<string, unknown>>(\n () => ({\n folder,\n page,\n search: search.trim(),\n filters: filterValues,\n retryLastMutation,\n }),\n [filterValues, folder, page, retryLastMutation, search],\n )\n\n const executeBulkAction = React.useCallback(async (\n actionId: MessageBulkActionId,\n selectedRows: T[],\n ): Promise<boolean> => {\n const messageIds = selectedRows.map((row) => row.id).filter((id) => id.trim().length > 0)\n if (messageIds.length === 0) return false\n\n if (actionId === 'delete') {\n const confirmed = await confirm({\n title: t('messages.bulk.delete.title', 'Delete {count} messages?', { count: messageIds.length }),\n description: t('messages.bulk.delete.description', 'This removes the selected messages from your view.'),\n confirmText: t('messages.actions.delete', 'Delete'),\n variant: 'destructive',\n })\n if (!confirmed) return false\n }\n\n const requestConfig = MESSAGE_BULK_REQUESTS[actionId]\n\n try {\n const summary = await runMutation<BulkExecutionSummary>({\n operation: async () => {\n const results = await runWithConcurrency(messageIds, 5, async (messageId) => {\n const call = await apiCall<{ ok?: boolean }>(requestConfig.buildUrl(messageId), {\n method: requestConfig.method,\n })\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t(requestConfig.errorKey, requestConfig.errorFallback),\n )\n }\n })\n\n const failed = results.filter((result) => result.status === 'rejected').length\n const succeeded = results.length - failed\n\n if (succeeded > 0) {\n await queryClient.invalidateQueries({ queryKey: ['messages', 'list'] })\n }\n\n return {\n action: actionId,\n total: messageIds.length,\n succeeded,\n failed,\n }\n },\n context: {\n actionId,\n messageIds,\n folder,\n page,\n search: search.trim(),\n filters: filterValues,\n retryLastMutation,\n },\n mutationPayload: {\n actionId,\n messageIds,\n },\n })\n\n if (summary.succeeded === 0) {\n flash(\n t('messages.bulk.flash.failed', 'Failed to process {count} messages.', { count: summary.failed }),\n 'error',\n )\n return false\n }\n\n if (summary.failed > 0) {\n flash(\n t('messages.bulk.flash.partial', '{succeeded} of {total} messages processed; {failed} failed.', {\n succeeded: summary.succeeded,\n total: summary.total,\n failed: summary.failed,\n }),\n 'warning',\n )\n return true\n }\n\n flash(\n t(requestConfig.successKey, requestConfig.successFallback, { count: summary.succeeded }),\n 'success',\n )\n return true\n } catch (error) {\n flash(\n error instanceof Error\n ? error.message\n : t(requestConfig.errorKey, requestConfig.errorFallback),\n 'error',\n )\n return false\n }\n }, [confirm, filterValues, folder, page, queryClient, retryLastMutation, runMutation, search, t])\n\n const bulkActions = React.useMemo<BulkAction<T>[] | undefined>(\n () => {\n const actionIds = getMessageBulkActionIdsForFolder(folder)\n if (actionIds.length === 0) return undefined\n\n return actionIds.map((actionId) => {\n const action = MESSAGE_BULK_ACTIONS[actionId]\n return {\n id: action.id,\n label: t(action.labelKey, action.labelFallback),\n destructive: action.destructive,\n onExecute: (selectedRows: T[]) => executeBulkAction(actionId, selectedRows),\n }\n })\n },\n [executeBulkAction, folder, t],\n )\n\n return {\n bulkActions,\n selectionScopeKey,\n injectionContext,\n ConfirmDialogElement,\n }\n}\n"],
|
|
5
|
-
"mappings": ";AAEA,YAAY,WAAW;AACvB,SAAS,sBAAsB;AAC/B,SAAS,YAAY;AAGrB,SAAS,wBAAwB;AACjC,SAAS,aAAa;AACtB,SAAS,0BAA0B;AACnC,SAAS,eAAe;AACxB,SAAS,sBAAsB;AAkD/B,MAAM,wBAA+E;AAAA,EACnF,UAAU;AAAA,IACR,QAAQ;AAAA,IACR,UAAU,CAAC,cAAc,iBAAiB,mBAAmB,SAAS,CAAC;AAAA,IACvE,YAAY;AAAA,IACZ,iBAAiB;AAAA,IACjB,UAAU;AAAA,IACV,eAAe;AAAA,EACjB;AAAA,EACA,YAAY;AAAA,IACV,QAAQ;AAAA,IACR,UAAU,CAAC,cAAc,iBAAiB,mBAAmB,SAAS,CAAC;AAAA,IACvE,YAAY;AAAA,IACZ,iBAAiB;AAAA,IACjB,UAAU;AAAA,IACV,eAAe;AAAA,EACjB;AAAA,EACA,SAAS;AAAA,IACP,QAAQ;AAAA,IACR,UAAU,CAAC,cAAc,iBAAiB,mBAAmB,SAAS,CAAC;AAAA,IACvE,YAAY;AAAA,IACZ,iBAAiB;AAAA,IACjB,UAAU;AAAA,IACV,eAAe;AAAA,EACjB;AAAA,EACA,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,UAAU,CAAC,cAAc,iBAAiB,mBAAmB,SAAS,CAAC;AAAA,IACvE,YAAY;AAAA,IACZ,iBAAiB;AAAA,IACjB,UAAU;AAAA,IACV,eAAe;AAAA,EACjB;AACF;AAEA,MAAM,uBAAiF;AAAA,EACrF,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,UAAU;AAAA,IACV,eAAe;AAAA,EACjB;AAAA,EACA,YAAY;AAAA,IACV,IAAI;AAAA,IACJ,UAAU;AAAA,IACV,eAAe;AAAA,EACjB;AAAA,EACA,SAAS;AAAA,IACP,IAAI;AAAA,IACJ,UAAU;AAAA,IACV,eAAe;AAAA,EACjB;AAAA,EACA,QAAQ;AAAA,IACN,IAAI;AAAA,IACJ,UAAU;AAAA,IACV,eAAe;AAAA,IACf,aAAa;AAAA,EACf;AACF;AAEA,MAAM,oCAAoC;AAAA,EACxC,OAAO,CAAC,YAAY,cAAc,WAAW,QAAQ;AAAA,EACrD,UAAU,CAAC,YAAY,cAAc,QAAQ;AAAA,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useQueryClient } from '@tanstack/react-query'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { BulkAction } from '@open-mercato/ui/backend/DataTable'\nimport type { FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { toErrorMessage } from './message-detail/utils'\n\nexport type MessageFolder = 'inbox' | 'sent' | 'drafts' | 'archived' | 'all'\n\nexport type MessageBulkActionId = 'markRead' | 'markUnread' | 'archive' | 'unarchive' | 'delete'\n\ntype BulkExecutionSummary = {\n action: MessageBulkActionId\n total: number\n succeeded: number\n failed: number\n}\n\ntype MessageBulkRequestConfig = {\n method: 'PUT' | 'DELETE'\n buildUrl: (messageId: string) => string\n successKey: string\n successFallback: string\n errorKey: string\n errorFallback: string\n}\n\ntype MessageBulkActionDefinition = {\n id: string\n labelKey: string\n labelFallback: string\n destructive?: boolean\n}\n\ntype MessageInboxBulkMutationContext = {\n actionId: MessageBulkActionId\n messageIds: string[]\n folder: MessageFolder\n page: number\n search: string\n filters: FilterValues\n retryLastMutation: () => Promise<boolean>\n}\n\ntype UseMessagesInboxBulkActionsInput = {\n folder: MessageFolder\n page: number\n search: string\n filterValues: FilterValues\n}\n\ntype MessageInboxBulkRow = {\n id: string\n}\n\nconst MESSAGE_BULK_REQUESTS: Record<MessageBulkActionId, MessageBulkRequestConfig> = {\n markRead: {\n method: 'PUT',\n buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/read`,\n successKey: 'messages.bulk.flash.markReadSuccess',\n successFallback: '{count} messages marked as read.',\n errorKey: 'messages.errors.stateChangeFailed',\n errorFallback: 'Failed to update message state.',\n },\n markUnread: {\n method: 'DELETE',\n buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/read`,\n successKey: 'messages.bulk.flash.markUnreadSuccess',\n successFallback: '{count} messages marked as unread.',\n errorKey: 'messages.errors.stateChangeFailed',\n errorFallback: 'Failed to update message state.',\n },\n archive: {\n method: 'PUT',\n buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/archive`,\n successKey: 'messages.bulk.flash.archiveSuccess',\n successFallback: '{count} messages archived.',\n errorKey: 'messages.errors.stateChangeFailed',\n errorFallback: 'Failed to update message state.',\n },\n unarchive: {\n method: 'DELETE',\n buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/archive`,\n successKey: 'messages.bulk.flash.unarchiveSuccess',\n successFallback: '{count} messages moved to inbox.',\n errorKey: 'messages.errors.stateChangeFailed',\n errorFallback: 'Failed to update message state.',\n },\n delete: {\n method: 'DELETE',\n buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}`,\n successKey: 'messages.bulk.flash.deleteSuccess',\n successFallback: '{count} messages deleted.',\n errorKey: 'messages.errors.deleteFailed',\n errorFallback: 'Failed to delete message.',\n },\n}\n\nconst MESSAGE_BULK_ACTIONS: Record<MessageBulkActionId, MessageBulkActionDefinition> = {\n markRead: {\n id: 'messages-mark-read',\n labelKey: 'messages.actions.markRead',\n labelFallback: 'Mark read',\n },\n markUnread: {\n id: 'messages-mark-unread',\n labelKey: 'messages.actions.markUnread',\n labelFallback: 'Mark unread',\n },\n archive: {\n id: 'messages-archive',\n labelKey: 'messages.actions.archive',\n labelFallback: 'Archive',\n },\n unarchive: {\n id: 'messages-unarchive',\n labelKey: 'messages.actions.unarchive',\n labelFallback: 'Unarchive',\n },\n delete: {\n id: 'messages-delete',\n labelKey: 'messages.actions.delete',\n labelFallback: 'Delete',\n destructive: true,\n },\n}\n\nconst MESSAGE_BULK_ACTION_IDS_BY_FOLDER = {\n inbox: ['markRead', 'markUnread', 'archive', 'delete'],\n archived: ['markRead', 'markUnread', 'unarchive', 'delete'],\n sent: ['delete'],\n drafts: ['delete'],\n all: [],\n} satisfies Record<MessageFolder, readonly MessageBulkActionId[]>\n\nexport function getMessageBulkActionIdsForFolder(folder: MessageFolder): MessageBulkActionId[] {\n return [...MESSAGE_BULK_ACTION_IDS_BY_FOLDER[folder]]\n}\n\nfunction normalizeSelectionScopeValue(value: unknown): unknown {\n if (value == null) return undefined\n if (typeof value === 'string') {\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n }\n if (Array.isArray(value)) {\n const normalized = value\n .map((item) => normalizeSelectionScopeValue(item))\n .filter((item) => item !== undefined)\n return normalized.length > 0 ? normalized : undefined\n }\n if (typeof value === 'object') {\n const normalizedEntries = Object.entries(value)\n .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))\n .map(([key, nestedValue]) => [key, normalizeSelectionScopeValue(nestedValue)] as const)\n .filter(([, nestedValue]) => nestedValue !== undefined)\n if (normalizedEntries.length === 0) return undefined\n return Object.fromEntries(normalizedEntries)\n }\n return value\n}\n\nfunction buildMessageSelectionScopeKey(\n folder: MessageFolder,\n page: number,\n search: string,\n filterValues: FilterValues,\n): string {\n return JSON.stringify({\n folder,\n page,\n search: search.trim(),\n filters: normalizeSelectionScopeValue(filterValues) ?? {},\n })\n}\n\nasync function runWithConcurrency<TItem>(\n items: TItem[],\n limit: number,\n worker: (item: TItem) => Promise<void>,\n): Promise<PromiseSettledResult<void>[]> {\n if (items.length === 0) return []\n\n const results: PromiseSettledResult<void>[] = new Array(items.length)\n let nextIndex = 0\n\n const runWorker = async () => {\n while (nextIndex < items.length) {\n const currentIndex = nextIndex\n nextIndex += 1\n try {\n await worker(items[currentIndex])\n results[currentIndex] = { status: 'fulfilled', value: undefined }\n } catch (error) {\n results[currentIndex] = { status: 'rejected', reason: error }\n }\n }\n }\n\n await Promise.all(\n Array.from({ length: Math.min(limit, items.length) }, () => runWorker()),\n )\n\n return results\n}\n\nexport function useMessagesInboxBulkActions<T extends MessageInboxBulkRow>({\n folder,\n page,\n search,\n filterValues,\n}: UseMessagesInboxBulkActionsInput): {\n bulkActions: BulkAction<T>[] | undefined\n selectionScopeKey: string\n injectionContext: Record<string, unknown>\n ConfirmDialogElement: React.ReactNode\n} {\n const t = useT()\n const queryClient = useQueryClient()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const { runMutation, retryLastMutation } = useGuardedMutation<MessageInboxBulkMutationContext>({\n contextId: 'messages-inbox-bulk-actions',\n })\n\n const selectionScopeKey = React.useMemo(\n () => buildMessageSelectionScopeKey(folder, page, search, filterValues),\n [filterValues, folder, page, search],\n )\n const injectionContext = React.useMemo<Record<string, unknown>>(\n () => ({\n folder,\n page,\n search: search.trim(),\n filters: filterValues,\n retryLastMutation,\n }),\n [filterValues, folder, page, retryLastMutation, search],\n )\n\n const executeBulkAction = React.useCallback(async (\n actionId: MessageBulkActionId,\n selectedRows: T[],\n ): Promise<boolean> => {\n const messageIds = selectedRows.map((row) => row.id).filter((id) => id.trim().length > 0)\n if (messageIds.length === 0) return false\n\n if (actionId === 'delete') {\n const confirmed = await confirm({\n title: t('messages.bulk.delete.title', 'Delete {count} messages?', { count: messageIds.length }),\n description: t('messages.bulk.delete.description', 'This removes the selected messages from your view.'),\n confirmText: t('messages.actions.delete', 'Delete'),\n variant: 'destructive',\n })\n if (!confirmed) return false\n }\n\n const requestConfig = MESSAGE_BULK_REQUESTS[actionId]\n\n try {\n const summary = await runMutation<BulkExecutionSummary>({\n operation: async () => {\n const results = await runWithConcurrency(messageIds, 5, async (messageId) => {\n const call = await apiCall<{ ok?: boolean }>(requestConfig.buildUrl(messageId), {\n method: requestConfig.method,\n })\n if (!call.ok) {\n throw new Error(\n toErrorMessage(call.result)\n ?? t(requestConfig.errorKey, requestConfig.errorFallback),\n )\n }\n })\n\n const failed = results.filter((result) => result.status === 'rejected').length\n const succeeded = results.length - failed\n\n if (succeeded > 0) {\n await queryClient.invalidateQueries({ queryKey: ['messages', 'list'] })\n }\n\n return {\n action: actionId,\n total: messageIds.length,\n succeeded,\n failed,\n }\n },\n context: {\n actionId,\n messageIds,\n folder,\n page,\n search: search.trim(),\n filters: filterValues,\n retryLastMutation,\n },\n mutationPayload: {\n actionId,\n messageIds,\n },\n })\n\n if (summary.succeeded === 0) {\n flash(\n t('messages.bulk.flash.failed', 'Failed to process {count} messages.', { count: summary.failed }),\n 'error',\n )\n return false\n }\n\n if (summary.failed > 0) {\n flash(\n t('messages.bulk.flash.partial', '{succeeded} of {total} messages processed; {failed} failed.', {\n succeeded: summary.succeeded,\n total: summary.total,\n failed: summary.failed,\n }),\n 'warning',\n )\n return true\n }\n\n flash(\n t(requestConfig.successKey, requestConfig.successFallback, { count: summary.succeeded }),\n 'success',\n )\n return true\n } catch (error) {\n flash(\n error instanceof Error\n ? error.message\n : t(requestConfig.errorKey, requestConfig.errorFallback),\n 'error',\n )\n return false\n }\n }, [confirm, filterValues, folder, page, queryClient, retryLastMutation, runMutation, search, t])\n\n const bulkActions = React.useMemo<BulkAction<T>[] | undefined>(\n () => {\n const actionIds = getMessageBulkActionIdsForFolder(folder)\n if (actionIds.length === 0) return undefined\n\n return actionIds.map((actionId) => {\n const action = MESSAGE_BULK_ACTIONS[actionId]\n return {\n id: action.id,\n label: t(action.labelKey, action.labelFallback),\n destructive: action.destructive,\n onExecute: (selectedRows: T[]) => executeBulkAction(actionId, selectedRows),\n }\n })\n },\n [executeBulkAction, folder, t],\n )\n\n return {\n bulkActions,\n selectionScopeKey,\n injectionContext,\n ConfirmDialogElement,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,6 +3,7 @@ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
|
3
3
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
4
4
|
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
5
5
|
import { GatewayTransaction } from "../../../data/entities.js";
|
|
6
|
+
import { readGatewayMetadata, readWebhookLog } from "../../../lib/transaction-fields.js";
|
|
6
7
|
import { paymentGatewaysTag } from "../../openapi.js";
|
|
7
8
|
const metadata = {
|
|
8
9
|
path: "/payment_gateways/transactions/[id]",
|
|
@@ -72,8 +73,8 @@ async function GET(req, { params }) {
|
|
|
72
73
|
redirectUrl: transaction.redirectUrl ?? null,
|
|
73
74
|
amount: transaction.amount,
|
|
74
75
|
currencyCode: transaction.currencyCode,
|
|
75
|
-
gatewayMetadata: transaction.gatewayMetadata
|
|
76
|
-
webhookLog:
|
|
76
|
+
gatewayMetadata: transaction.gatewayMetadata == null ? null : readGatewayMetadata(transaction.gatewayMetadata),
|
|
77
|
+
webhookLog: readWebhookLog(transaction.webhookLog),
|
|
77
78
|
lastWebhookAt: toIsoString(transaction.lastWebhookAt),
|
|
78
79
|
lastPolledAt: toIsoString(transaction.lastPolledAt),
|
|
79
80
|
expiresAt: toIsoString(transaction.expiresAt),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/payment_gateways/api/transactions/%5Bid%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { IntegrationLogService } from '../../../../integrations/lib/log-service'\nimport { GatewayTransaction } from '../../../data/entities'\nimport { paymentGatewaysTag } from '../../openapi'\n\nexport const metadata = {\n path: '/payment_gateways/transactions/[id]',\n GET: { requireAuth: true, requireFeatures: ['payment_gateways.view'] },\n}\n\nfunction toIsoString(value: unknown): string | null {\n if (!value) return null\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'string') {\n const parsed = new Date(value)\n if (!Number.isNaN(parsed.getTime())) return parsed.toISOString()\n return value\n }\n const parsed = new Date(value as string | number)\n return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString()\n}\n\nexport async function GET(req: Request, { params }: { params: Promise<{ id: string }> | { id: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const resolvedParams = await params\n const transactionId = resolvedParams?.id\n if (!transactionId) {\n return NextResponse.json({ error: 'Transaction id is required' }, { status: 400 })\n }\n\n const { resolve } = await createRequestContainer()\n const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }\n const em = resolve('em') as EntityManager\n const integrationLogService = resolve('integrationLogService') as IntegrationLogService\n const transaction = await findOneWithDecryption(\n em,\n GatewayTransaction,\n {\n id: transactionId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (!transaction) {\n return NextResponse.json({ error: 'Transaction not found' }, { status: 404 })\n }\n\n const integrationId = `gateway_${transaction.providerKey}`\n const { items: logRows } = await integrationLogService.query(\n {\n integrationId,\n entityType: 'payment_transaction',\n entityId: transactionId,\n page: 1,\n pageSize: 100,\n },\n scope,\n )\n\n return NextResponse.json({\n transaction: {\n id: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n providerSessionId: transaction.providerSessionId ?? null,\n gatewayPaymentId: transaction.gatewayPaymentId ?? null,\n gatewayRefundId: transaction.gatewayRefundId ?? null,\n unifiedStatus: transaction.unifiedStatus,\n gatewayStatus: transaction.gatewayStatus ?? null,\n redirectUrl: transaction.redirectUrl ?? null,\n amount: transaction.amount,\n currencyCode: transaction.currencyCode,\n gatewayMetadata: transaction.gatewayMetadata
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,6BAA6B;AAEtC,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AAE5B,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,uBAAuB,EAAE;AACvE;AAEA,SAAS,YAAY,OAA+B;AAClD,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,iBAAiB,KAAM,QAAO,MAAM,YAAY;AACpD,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAMA,UAAS,IAAI,KAAK,KAAK;AAC7B,QAAI,CAAC,OAAO,MAAMA,QAAO,QAAQ,CAAC,EAAG,QAAOA,QAAO,YAAY;AAC/D,WAAO;AAAA,EACT;AACA,QAAM,SAAS,IAAI,KAAK,KAAwB;AAChD,SAAO,OAAO,MAAM,OAAO,QAAQ,CAAC,IAAI,OAAO,OAAO,YAAY;AACpE;AAEA,eAAsB,IAAI,KAAc,EAAE,OAAO,GAAyD;AACxG,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,YAAY,CAAC,KAAK,OAAO;AAClC,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,QAAM,iBAAiB,MAAM;AAC7B,QAAM,gBAAgB,gBAAgB;AACtC,MAAI,CAAC,eAAe;AAClB,WAAO,aAAa,KAAK,EAAE,OAAO,6BAA6B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACnF;AAEA,QAAM,EAAE,QAAQ,IAAI,MAAM,uBAAuB;AACjD,QAAM,QAAQ,EAAE,gBAAgB,KAAK,OAAiB,UAAU,KAAK,SAAS;AAC9E,QAAM,KAAK,QAAQ,IAAI;AACvB,QAAM,wBAAwB,QAAQ,uBAAuB;AAC7D,QAAM,cAAc,MAAM;AAAA,IACxB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,MAChB,WAAW;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,MAAI,CAAC,aAAa;AAChB,WAAO,aAAa,KAAK,EAAE,OAAO,wBAAwB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC9E;AAEA,QAAM,gBAAgB,WAAW,YAAY,WAAW;AACxD,QAAM,EAAE,OAAO,QAAQ,IAAI,MAAM,sBAAsB;AAAA,IACrD;AAAA,MACE;AAAA,MACA,YAAY;AAAA,MACZ,UAAU;AAAA,MACV,MAAM;AAAA,MACN,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,EACF;AAEA,SAAO,aAAa,KAAK;AAAA,IACvB,aAAa;AAAA,MACX,IAAI,YAAY;AAAA,MAChB,WAAW,YAAY;AAAA,MACvB,aAAa,YAAY;AAAA,MACzB,mBAAmB,YAAY,qBAAqB;AAAA,MACpD,kBAAkB,YAAY,oBAAoB;AAAA,MAClD,iBAAiB,YAAY,mBAAmB;AAAA,MAChD,eAAe,YAAY;AAAA,MAC3B,eAAe,YAAY,iBAAiB;AAAA,MAC5C,aAAa,YAAY,eAAe;AAAA,MACxC,QAAQ,YAAY;AAAA,MACpB,cAAc,YAAY;AAAA,MAC1B,iBAAiB,YAAY,mBAAmB
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { IntegrationLogService } from '../../../../integrations/lib/log-service'\nimport { GatewayTransaction } from '../../../data/entities'\nimport { readGatewayMetadata, readWebhookLog } from '../../../lib/transaction-fields'\nimport { paymentGatewaysTag } from '../../openapi'\n\nexport const metadata = {\n path: '/payment_gateways/transactions/[id]',\n GET: { requireAuth: true, requireFeatures: ['payment_gateways.view'] },\n}\n\nfunction toIsoString(value: unknown): string | null {\n if (!value) return null\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'string') {\n const parsed = new Date(value)\n if (!Number.isNaN(parsed.getTime())) return parsed.toISOString()\n return value\n }\n const parsed = new Date(value as string | number)\n return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString()\n}\n\nexport async function GET(req: Request, { params }: { params: Promise<{ id: string }> | { id: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const resolvedParams = await params\n const transactionId = resolvedParams?.id\n if (!transactionId) {\n return NextResponse.json({ error: 'Transaction id is required' }, { status: 400 })\n }\n\n const { resolve } = await createRequestContainer()\n const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }\n const em = resolve('em') as EntityManager\n const integrationLogService = resolve('integrationLogService') as IntegrationLogService\n const transaction = await findOneWithDecryption(\n em,\n GatewayTransaction,\n {\n id: transactionId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (!transaction) {\n return NextResponse.json({ error: 'Transaction not found' }, { status: 404 })\n }\n\n const integrationId = `gateway_${transaction.providerKey}`\n const { items: logRows } = await integrationLogService.query(\n {\n integrationId,\n entityType: 'payment_transaction',\n entityId: transactionId,\n page: 1,\n pageSize: 100,\n },\n scope,\n )\n\n return NextResponse.json({\n transaction: {\n id: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n providerSessionId: transaction.providerSessionId ?? null,\n gatewayPaymentId: transaction.gatewayPaymentId ?? null,\n gatewayRefundId: transaction.gatewayRefundId ?? null,\n unifiedStatus: transaction.unifiedStatus,\n gatewayStatus: transaction.gatewayStatus ?? null,\n redirectUrl: transaction.redirectUrl ?? null,\n amount: transaction.amount,\n currencyCode: transaction.currencyCode,\n gatewayMetadata: transaction.gatewayMetadata == null ? null : readGatewayMetadata(transaction.gatewayMetadata),\n webhookLog: readWebhookLog(transaction.webhookLog),\n lastWebhookAt: toIsoString(transaction.lastWebhookAt),\n lastPolledAt: toIsoString(transaction.lastPolledAt),\n expiresAt: toIsoString(transaction.expiresAt),\n createdAt: toIsoString(transaction.createdAt),\n updatedAt: toIsoString(transaction.updatedAt),\n },\n logs: logRows.map((row) => ({\n id: row.id,\n integrationId: row.integrationId,\n runId: row.runId ?? null,\n scopeEntityType: row.scopeEntityType ?? null,\n scopeEntityId: row.scopeEntityId ?? null,\n level: row.level,\n message: row.message,\n code: row.code ?? null,\n payload: row.payload ?? null,\n createdAt: toIsoString(row.createdAt),\n })),\n })\n}\n\nexport const openApi = {\n tags: [paymentGatewaysTag],\n summary: 'Get payment transaction details',\n methods: {\n GET: {\n summary: 'Get payment transaction details',\n tags: [paymentGatewaysTag],\n responses: [\n { status: 200, description: 'Payment transaction details' },\n { status: 404, description: 'Transaction not found' },\n ],\n },\n },\n}\n\nexport default GET\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["parsed"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const defaultEncryptionMaps = [
|
|
2
|
+
{
|
|
3
|
+
entityId: "payment_gateways:gateway_transaction",
|
|
4
|
+
fields: [
|
|
5
|
+
{ field: "client_secret" },
|
|
6
|
+
{ field: "gateway_metadata" },
|
|
7
|
+
{ field: "webhook_log" }
|
|
8
|
+
]
|
|
9
|
+
}
|
|
10
|
+
];
|
|
11
|
+
var encryption_default = defaultEncryptionMaps;
|
|
12
|
+
export {
|
|
13
|
+
encryption_default as default,
|
|
14
|
+
defaultEncryptionMaps
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=encryption.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/payment_gateways/encryption.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'\n\n// Gateway transactions persist provider secrets and raw provider payloads. Encrypt the client\n// secret plus the JSONB columns that carry provider/session data and webhook history at rest.\n// Lookup-critical columns (provider_key, provider_session_id, unified_status, gateway_payment_id,\n// gateway_refund_id) are intentionally left in plaintext so status filters and session lookups keep\n// working without a deterministic hash. The JSONB fields decrypt back to strings (entity fields are\n// never auto-parsed \u2014 see issue #1810), so consumers MUST normalize them via lib/transaction-fields.\nexport const defaultEncryptionMaps: ModuleEncryptionMap[] = [\n {\n entityId: 'payment_gateways:gateway_transaction',\n fields: [\n { field: 'client_secret' },\n { field: 'gateway_metadata' },\n { field: 'webhook_log' },\n ],\n },\n]\n\nexport default defaultEncryptionMaps\n"],
|
|
5
|
+
"mappings": "AAQO,MAAM,wBAA+C;AAAA,EAC1D;AAAA,IACE,UAAU;AAAA,IACV,QAAQ;AAAA,MACN,EAAE,OAAO,gBAAgB;AAAA,MACzB,EAAE,OAAO,mBAAmB;AAAA,MAC5B,EAAE,OAAO,cAAc;AAAA,IACzB;AAAA,EACF;AACF;AAEA,IAAO,qBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -6,6 +6,7 @@ import { conflict } from "@open-mercato/shared/lib/crud/errors";
|
|
|
6
6
|
import { GatewayTransaction } from "../data/entities.js";
|
|
7
7
|
import { canApplyManualAction, isValidTransition } from "./status-machine.js";
|
|
8
8
|
import { emitPaymentGatewayEvent } from "../events.js";
|
|
9
|
+
import { readGatewayMetadata, readWebhookLog } from "./transaction-fields.js";
|
|
9
10
|
function assertManualActionAllowed(action, transaction) {
|
|
10
11
|
const current = transaction.unifiedStatus;
|
|
11
12
|
if (!canApplyManualAction(action, current)) {
|
|
@@ -160,7 +161,7 @@ function createPaymentGatewayService(deps) {
|
|
|
160
161
|
credentials
|
|
161
162
|
});
|
|
162
163
|
const statusChanged = applyAdapterResultStatus("capture", transaction, result.status);
|
|
163
|
-
transaction.gatewayMetadata = { ...transaction.gatewayMetadata, captureResult: result.providerData };
|
|
164
|
+
transaction.gatewayMetadata = { ...readGatewayMetadata(transaction.gatewayMetadata), captureResult: result.providerData };
|
|
164
165
|
await em.flush();
|
|
165
166
|
if (statusChanged) {
|
|
166
167
|
await emitStatusEvent(result.status, {
|
|
@@ -200,7 +201,7 @@ function createPaymentGatewayService(deps) {
|
|
|
200
201
|
});
|
|
201
202
|
const statusChanged = applyAdapterResultStatus("refund", transaction, result.status);
|
|
202
203
|
transaction.gatewayRefundId = result.refundId;
|
|
203
|
-
transaction.gatewayMetadata = { ...transaction.gatewayMetadata, refundResult: result.providerData };
|
|
204
|
+
transaction.gatewayMetadata = { ...readGatewayMetadata(transaction.gatewayMetadata), refundResult: result.providerData };
|
|
204
205
|
await em.flush();
|
|
205
206
|
if (statusChanged) {
|
|
206
207
|
await emitStatusEvent(result.status, {
|
|
@@ -276,7 +277,7 @@ function createPaymentGatewayService(deps) {
|
|
|
276
277
|
const previousStatus = transaction.unifiedStatus;
|
|
277
278
|
transaction.unifiedStatus = status.status;
|
|
278
279
|
transaction.gatewayStatus = status.status;
|
|
279
|
-
transaction.gatewayMetadata = { ...transaction.gatewayMetadata, statusResult: status.providerData ?? null };
|
|
280
|
+
transaction.gatewayMetadata = { ...readGatewayMetadata(transaction.gatewayMetadata), statusResult: status.providerData ?? null };
|
|
280
281
|
transaction.lastPolledAt = /* @__PURE__ */ new Date();
|
|
281
282
|
await em.flush();
|
|
282
283
|
await emitStatusEvent(status.status, {
|
|
@@ -314,10 +315,10 @@ function createPaymentGatewayService(deps) {
|
|
|
314
315
|
transaction.gatewayStatus = update.providerStatus;
|
|
315
316
|
}
|
|
316
317
|
if (update.providerData) {
|
|
317
|
-
transaction.gatewayMetadata = { ...transaction.gatewayMetadata, ...update.providerData };
|
|
318
|
+
transaction.gatewayMetadata = { ...readGatewayMetadata(transaction.gatewayMetadata), ...update.providerData };
|
|
318
319
|
}
|
|
319
320
|
if (update.webhookEvent) {
|
|
320
|
-
const webhookLog =
|
|
321
|
+
const webhookLog = readWebhookLog(transaction.webhookLog);
|
|
321
322
|
webhookLog.push({
|
|
322
323
|
eventType: update.webhookEvent.eventType,
|
|
323
324
|
receivedAt: update.webhookEvent.receivedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/payment_gateways/lib/gateway-service.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n getGatewayAdapter,\n type CreateSessionInput,\n type CreateSessionResult,\n type CaptureResult,\n type RefundResult,\n type CancelResult,\n type GatewayPaymentStatus,\n type PaymentGatewayPresentationRequest,\n type UnifiedPaymentStatus,\n} from '@open-mercato/shared/modules/payment_gateways/types'\nimport type { CredentialsService } from '../../integrations/lib/credentials-service'\nimport type { IntegrationStateService } from '../../integrations/lib/state-service'\nimport type { IntegrationLogService } from '../../integrations/lib/log-service'\nimport { conflict } from '@open-mercato/shared/lib/crud/errors'\nimport { GatewayTransaction } from '../data/entities'\nimport { canApplyManualAction, isValidTransition, type ManualGatewayAction } from './status-machine'\nimport { emitPaymentGatewayEvent } from '../events'\n\nfunction assertManualActionAllowed(action: ManualGatewayAction, transaction: GatewayTransaction): void {\n const current = transaction.unifiedStatus as UnifiedPaymentStatus\n if (!canApplyManualAction(action, current)) {\n throw conflict(`Cannot ${action} a payment in status \"${current}\"`)\n }\n}\n\nfunction applyAdapterResultStatus(\n action: ManualGatewayAction,\n transaction: GatewayTransaction,\n resultStatus: UnifiedPaymentStatus,\n): boolean {\n const current = transaction.unifiedStatus as UnifiedPaymentStatus\n if (resultStatus === current) return false\n if (!isValidTransition(current, resultStatus)) {\n throw conflict(\n `Gateway returned status \"${resultStatus}\" which is not a valid transition from \"${current}\" for ${action}`,\n )\n }\n transaction.unifiedStatus = resultStatus\n return true\n}\n\nexport interface PaymentGatewayServiceDeps {\n em: EntityManager\n integrationCredentialsService: CredentialsService\n integrationStateService?: IntegrationStateService\n integrationLogService?: IntegrationLogService\n}\n\nexport interface CreatePaymentSessionInput {\n providerKey: string\n paymentId: string\n orderId?: string\n amount: number\n currencyCode: string\n captureMethod?: 'automatic' | 'manual'\n paymentTypes?: string[]\n description?: string\n successUrl?: string\n cancelUrl?: string\n metadata?: Record<string, unknown>\n presentation?: PaymentGatewayPresentationRequest\n organizationId: string\n tenantId: string\n}\n\nexport function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {\n const { em, integrationCredentialsService, integrationLogService } = deps\n\n async function findTransactionOrThrow(\n transactionId: string,\n scope: { organizationId: string; tenantId: string },\n ): Promise<GatewayTransaction> {\n const transaction = await findOneWithDecryption(\n em,\n GatewayTransaction,\n {\n id: transactionId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!transaction) {\n throw new Error('Transaction not found')\n }\n return transaction\n }\n\n function readProviderSessionId(transaction: GatewayTransaction): string {\n if (typeof transaction.providerSessionId === 'string' && transaction.providerSessionId.trim().length > 0) {\n return transaction.providerSessionId\n }\n throw new Error('Transaction is missing provider session id')\n }\n\n async function emitStatusEvent(status: UnifiedPaymentStatus, payload: Record<string, unknown>) {\n type PaymentGatewayEventId = Parameters<typeof emitPaymentGatewayEvent>[0]\n const eventMap: Partial<Record<UnifiedPaymentStatus, PaymentGatewayEventId>> = {\n authorized: 'payment_gateways.payment.authorized',\n captured: 'payment_gateways.payment.captured',\n failed: 'payment_gateways.payment.failed',\n refunded: 'payment_gateways.payment.refunded',\n cancelled: 'payment_gateways.payment.cancelled',\n }\n const eventId = eventMap[status]\n if (!eventId) return\n await emitPaymentGatewayEvent(eventId, payload)\n }\n\n async function writeTransactionLog(\n providerKey: string,\n scope: { organizationId: string; tenantId: string },\n transactionId: string,\n level: 'info' | 'warn' | 'error',\n message: string,\n payload?: Record<string, unknown> | null,\n code?: string | null,\n ) {\n if (!integrationLogService) return\n await integrationLogService.write({\n integrationId: `gateway_${providerKey}`,\n scopeEntityType: 'payment_transaction',\n scopeEntityId: transactionId,\n level,\n message,\n code,\n payload: payload ?? null,\n }, scope)\n }\n\n async function resolveAdapterAndCredentials(providerKey: string, scope: { organizationId: string; tenantId: string }) {\n const integrationId = `gateway_${providerKey}`\n const selectedVersion = deps.integrationStateService\n ? await deps.integrationStateService.resolveApiVersion(integrationId, scope)\n : undefined\n const adapter = getGatewayAdapter(providerKey, selectedVersion)\n if (!adapter) {\n throw new Error(\n selectedVersion\n ? `No gateway adapter registered for provider: ${providerKey} (version: ${selectedVersion})`\n : `No gateway adapter registered for provider: ${providerKey}`,\n )\n }\n const credentials = await integrationCredentialsService.resolve(integrationId, scope) ?? {}\n\n return { adapter, credentials }\n }\n\n return {\n async createPaymentSession(input: CreatePaymentSessionInput): Promise<{ transaction: GatewayTransaction; session: CreateSessionResult }> {\n const scope = { organizationId: input.organizationId, tenantId: input.tenantId }\n const { adapter, credentials } = await resolveAdapterAndCredentials(input.providerKey, scope)\n\n const sessionInput: CreateSessionInput = {\n paymentId: input.paymentId,\n orderId: input.orderId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n amount: input.amount,\n currencyCode: input.currencyCode,\n captureMethod: input.captureMethod,\n paymentTypes: input.paymentTypes,\n description: input.description,\n successUrl: input.successUrl,\n cancelUrl: input.cancelUrl,\n metadata: input.metadata,\n presentation: input.presentation,\n credentials,\n }\n\n const session = await adapter.createSession(sessionInput)\n\n const transaction = em.create(GatewayTransaction, {\n paymentId: input.paymentId,\n providerKey: input.providerKey,\n providerSessionId: session.sessionId,\n unifiedStatus: session.status,\n redirectUrl: session.redirectUrl\n ?? (session.clientSession?.type === 'redirect' ? session.clientSession.redirectUrl : null),\n clientSecret: session.clientSecret ?? null,\n amount: String(input.amount),\n currencyCode: input.currencyCode,\n gatewayMetadata: {\n ...(session.providerData ?? {}),\n ...(session.clientSession ? { clientSession: session.clientSession } : {}),\n },\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n })\n await em.persist(transaction).flush()\n await emitPaymentGatewayEvent('payment_gateways.session.created', {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n status: transaction.unifiedStatus,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n await writeTransactionLog(\n transaction.providerKey,\n scope,\n transaction.id,\n 'info',\n 'Payment session created',\n {\n paymentId: transaction.paymentId,\n providerSessionId: transaction.providerSessionId,\n status: transaction.unifiedStatus,\n amount: input.amount,\n currencyCode: input.currencyCode,\n },\n )\n\n return { transaction, session }\n },\n\n async capturePayment(transactionId: string, amount: number | undefined, scope: { organizationId: string; tenantId: string }): Promise<CaptureResult> {\n const transaction = await findTransactionOrThrow(transactionId, scope)\n assertManualActionAllowed('capture', transaction)\n const { adapter, credentials } = await resolveAdapterAndCredentials(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n )\n\n const result = await adapter.capture({\n sessionId: readProviderSessionId(transaction),\n amount,\n credentials,\n })\n\n const statusChanged = applyAdapterResultStatus('capture', transaction, result.status)\n transaction.gatewayMetadata = { ...transaction.gatewayMetadata, captureResult: result.providerData }\n await em.flush()\n if (statusChanged) {\n await emitStatusEvent(result.status, {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n }\n await writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'info',\n 'Payment captured',\n {\n amount: amount ?? null,\n status: result.status,\n capturedAmount: result.capturedAmount,\n },\n )\n\n return result\n },\n\n async refundPayment(\n transactionId: string,\n amount: number | undefined,\n reason: string | undefined,\n scope: { organizationId: string; tenantId: string },\n ): Promise<RefundResult> {\n const transaction = await findTransactionOrThrow(transactionId, scope)\n assertManualActionAllowed('refund', transaction)\n const { adapter, credentials } = await resolveAdapterAndCredentials(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n )\n\n const result = await adapter.refund({\n sessionId: readProviderSessionId(transaction),\n amount,\n reason,\n credentials,\n })\n\n const statusChanged = applyAdapterResultStatus('refund', transaction, result.status)\n transaction.gatewayRefundId = result.refundId\n transaction.gatewayMetadata = { ...transaction.gatewayMetadata, refundResult: result.providerData }\n await em.flush()\n if (statusChanged) {\n await emitStatusEvent(result.status, {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n }\n await writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'info',\n 'Payment refunded',\n {\n amount: amount ?? null,\n reason: reason ?? null,\n status: result.status,\n refundId: result.refundId,\n },\n )\n\n return result\n },\n\n async cancelPayment(\n transactionId: string,\n reason: string | undefined,\n scope: { organizationId: string; tenantId: string },\n ): Promise<CancelResult> {\n const transaction = await findTransactionOrThrow(transactionId, scope)\n assertManualActionAllowed('cancel', transaction)\n const { adapter, credentials } = await resolveAdapterAndCredentials(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n )\n\n const result = await adapter.cancel({\n sessionId: readProviderSessionId(transaction),\n reason,\n credentials,\n })\n\n const statusChanged = applyAdapterResultStatus('cancel', transaction, result.status)\n await em.flush()\n if (statusChanged) {\n await emitStatusEvent(result.status, {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n }\n await writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'info',\n 'Payment cancelled',\n {\n reason: reason ?? null,\n status: result.status,\n },\n )\n\n return result\n },\n\n async getPaymentStatus(transactionId: string, scope: { organizationId: string; tenantId: string }): Promise<GatewayPaymentStatus> {\n const transaction = await findTransactionOrThrow(transactionId, scope)\n const { adapter, credentials } = await resolveAdapterAndCredentials(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n )\n\n const status = await adapter.getStatus({\n sessionId: readProviderSessionId(transaction),\n credentials,\n })\n\n if (status.status !== transaction.unifiedStatus && isValidTransition(transaction.unifiedStatus as UnifiedPaymentStatus, status.status)) {\n const previousStatus = transaction.unifiedStatus\n transaction.unifiedStatus = status.status\n transaction.gatewayStatus = status.status\n transaction.gatewayMetadata = { ...transaction.gatewayMetadata, statusResult: status.providerData ?? null }\n transaction.lastPolledAt = new Date()\n await em.flush()\n await emitStatusEvent(status.status, {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n previousStatus,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n await writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'info',\n 'Payment status updated by poller',\n {\n previousStatus,\n nextStatus: status.status,\n },\n )\n }\n\n return status\n },\n\n async syncTransactionStatus(transactionId: string, update: {\n unifiedStatus: UnifiedPaymentStatus\n providerStatus?: string\n providerData?: Record<string, unknown>\n webhookEvent?: {\n eventType: string\n idempotencyKey: string\n processed: boolean\n receivedAt?: string\n }\n }, scope: { organizationId: string; tenantId: string }): Promise<void> {\n const transaction = await findTransactionOrThrow(transactionId, scope)\n const currentStatus = transaction.unifiedStatus as UnifiedPaymentStatus\n const canTransition = isValidTransition(currentStatus, update.unifiedStatus)\n const shouldApplyStatus = canTransition && update.unifiedStatus !== currentStatus\n const previousStatus = transaction.unifiedStatus\n if (shouldApplyStatus) {\n transaction.unifiedStatus = update.unifiedStatus\n }\n if (update.providerStatus) {\n transaction.gatewayStatus = update.providerStatus\n }\n if (update.providerData) {\n transaction.gatewayMetadata = { ...transaction.gatewayMetadata, ...update.providerData }\n }\n if (update.webhookEvent) {\n const webhookLog = Array.isArray(transaction.webhookLog) ? transaction.webhookLog : []\n webhookLog.push({\n eventType: update.webhookEvent.eventType,\n receivedAt: update.webhookEvent.receivedAt ?? new Date().toISOString(),\n idempotencyKey: update.webhookEvent.idempotencyKey,\n unifiedStatus: update.unifiedStatus,\n processed: update.webhookEvent.processed,\n })\n transaction.webhookLog = webhookLog\n }\n transaction.lastWebhookAt = new Date()\n await em.flush()\n if (shouldApplyStatus) {\n await emitStatusEvent(update.unifiedStatus, {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n previousStatus,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n }\n await writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n shouldApplyStatus ? 'info' : 'warn',\n shouldApplyStatus ? 'Payment status synchronized from webhook' : 'Webhook received with no status transition',\n {\n previousStatus,\n nextStatus: update.unifiedStatus,\n providerStatus: update.providerStatus ?? null,\n eventType: update.webhookEvent?.eventType ?? null,\n idempotencyKey: update.webhookEvent?.idempotencyKey ?? null,\n },\n )\n },\n\n async findTransaction(id: string, scope: { organizationId: string; tenantId: string }): Promise<GatewayTransaction | null> {\n return findOneWithDecryption(\n em,\n GatewayTransaction,\n {\n id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n },\n\n async findTransactionBySessionId(\n providerSessionId: string,\n scope: { organizationId: string; tenantId: string },\n providerKey?: string,\n ): Promise<GatewayTransaction | null> {\n return findOneWithDecryption(\n em,\n GatewayTransaction,\n {\n providerSessionId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n ...(providerKey ? { providerKey } : {}),\n },\n undefined,\n scope,\n )\n },\n\n async listTransactionsForStatusPolling(scope?: {\n organizationId?: string\n tenantId?: string\n providerKey?: string\n limit?: number\n }): Promise<GatewayTransaction[]> {\n const where: Record<string, unknown> = {\n unifiedStatus: { $in: ['pending', 'authorized', 'partially_captured'] },\n deletedAt: null,\n }\n if (scope?.organizationId) where.organizationId = scope.organizationId\n if (scope?.tenantId) where.tenantId = scope.tenantId\n if (scope?.providerKey) where.providerKey = scope.providerKey\n\n return findWithDecryption(\n em,\n GatewayTransaction,\n where,\n {\n orderBy: { updatedAt: 'asc' },\n limit: scope?.limit ?? 100,\n },\n scope,\n )\n },\n }\n}\n\nexport type PaymentGatewayService = ReturnType<typeof createPaymentGatewayService>\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,uBAAuB,0BAA0B;AAC1D;AAAA,EACE;AAAA,OASK;AAIP,SAAS,gBAAgB;AACzB,SAAS,0BAA0B;AACnC,SAAS,sBAAsB,yBAAmD;AAClF,SAAS,+BAA+B;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n getGatewayAdapter,\n type CreateSessionInput,\n type CreateSessionResult,\n type CaptureResult,\n type RefundResult,\n type CancelResult,\n type GatewayPaymentStatus,\n type PaymentGatewayPresentationRequest,\n type UnifiedPaymentStatus,\n} from '@open-mercato/shared/modules/payment_gateways/types'\nimport type { CredentialsService } from '../../integrations/lib/credentials-service'\nimport type { IntegrationStateService } from '../../integrations/lib/state-service'\nimport type { IntegrationLogService } from '../../integrations/lib/log-service'\nimport { conflict } from '@open-mercato/shared/lib/crud/errors'\nimport { GatewayTransaction } from '../data/entities'\nimport { canApplyManualAction, isValidTransition, type ManualGatewayAction } from './status-machine'\nimport { emitPaymentGatewayEvent } from '../events'\nimport { readGatewayMetadata, readWebhookLog } from './transaction-fields'\n\nfunction assertManualActionAllowed(action: ManualGatewayAction, transaction: GatewayTransaction): void {\n const current = transaction.unifiedStatus as UnifiedPaymentStatus\n if (!canApplyManualAction(action, current)) {\n throw conflict(`Cannot ${action} a payment in status \"${current}\"`)\n }\n}\n\nfunction applyAdapterResultStatus(\n action: ManualGatewayAction,\n transaction: GatewayTransaction,\n resultStatus: UnifiedPaymentStatus,\n): boolean {\n const current = transaction.unifiedStatus as UnifiedPaymentStatus\n if (resultStatus === current) return false\n if (!isValidTransition(current, resultStatus)) {\n throw conflict(\n `Gateway returned status \"${resultStatus}\" which is not a valid transition from \"${current}\" for ${action}`,\n )\n }\n transaction.unifiedStatus = resultStatus\n return true\n}\n\nexport interface PaymentGatewayServiceDeps {\n em: EntityManager\n integrationCredentialsService: CredentialsService\n integrationStateService?: IntegrationStateService\n integrationLogService?: IntegrationLogService\n}\n\nexport interface CreatePaymentSessionInput {\n providerKey: string\n paymentId: string\n orderId?: string\n amount: number\n currencyCode: string\n captureMethod?: 'automatic' | 'manual'\n paymentTypes?: string[]\n description?: string\n successUrl?: string\n cancelUrl?: string\n metadata?: Record<string, unknown>\n presentation?: PaymentGatewayPresentationRequest\n organizationId: string\n tenantId: string\n}\n\nexport function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {\n const { em, integrationCredentialsService, integrationLogService } = deps\n\n async function findTransactionOrThrow(\n transactionId: string,\n scope: { organizationId: string; tenantId: string },\n ): Promise<GatewayTransaction> {\n const transaction = await findOneWithDecryption(\n em,\n GatewayTransaction,\n {\n id: transactionId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!transaction) {\n throw new Error('Transaction not found')\n }\n return transaction\n }\n\n function readProviderSessionId(transaction: GatewayTransaction): string {\n if (typeof transaction.providerSessionId === 'string' && transaction.providerSessionId.trim().length > 0) {\n return transaction.providerSessionId\n }\n throw new Error('Transaction is missing provider session id')\n }\n\n async function emitStatusEvent(status: UnifiedPaymentStatus, payload: Record<string, unknown>) {\n type PaymentGatewayEventId = Parameters<typeof emitPaymentGatewayEvent>[0]\n const eventMap: Partial<Record<UnifiedPaymentStatus, PaymentGatewayEventId>> = {\n authorized: 'payment_gateways.payment.authorized',\n captured: 'payment_gateways.payment.captured',\n failed: 'payment_gateways.payment.failed',\n refunded: 'payment_gateways.payment.refunded',\n cancelled: 'payment_gateways.payment.cancelled',\n }\n const eventId = eventMap[status]\n if (!eventId) return\n await emitPaymentGatewayEvent(eventId, payload)\n }\n\n async function writeTransactionLog(\n providerKey: string,\n scope: { organizationId: string; tenantId: string },\n transactionId: string,\n level: 'info' | 'warn' | 'error',\n message: string,\n payload?: Record<string, unknown> | null,\n code?: string | null,\n ) {\n if (!integrationLogService) return\n await integrationLogService.write({\n integrationId: `gateway_${providerKey}`,\n scopeEntityType: 'payment_transaction',\n scopeEntityId: transactionId,\n level,\n message,\n code,\n payload: payload ?? null,\n }, scope)\n }\n\n async function resolveAdapterAndCredentials(providerKey: string, scope: { organizationId: string; tenantId: string }) {\n const integrationId = `gateway_${providerKey}`\n const selectedVersion = deps.integrationStateService\n ? await deps.integrationStateService.resolveApiVersion(integrationId, scope)\n : undefined\n const adapter = getGatewayAdapter(providerKey, selectedVersion)\n if (!adapter) {\n throw new Error(\n selectedVersion\n ? `No gateway adapter registered for provider: ${providerKey} (version: ${selectedVersion})`\n : `No gateway adapter registered for provider: ${providerKey}`,\n )\n }\n const credentials = await integrationCredentialsService.resolve(integrationId, scope) ?? {}\n\n return { adapter, credentials }\n }\n\n return {\n async createPaymentSession(input: CreatePaymentSessionInput): Promise<{ transaction: GatewayTransaction; session: CreateSessionResult }> {\n const scope = { organizationId: input.organizationId, tenantId: input.tenantId }\n const { adapter, credentials } = await resolveAdapterAndCredentials(input.providerKey, scope)\n\n const sessionInput: CreateSessionInput = {\n paymentId: input.paymentId,\n orderId: input.orderId,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n amount: input.amount,\n currencyCode: input.currencyCode,\n captureMethod: input.captureMethod,\n paymentTypes: input.paymentTypes,\n description: input.description,\n successUrl: input.successUrl,\n cancelUrl: input.cancelUrl,\n metadata: input.metadata,\n presentation: input.presentation,\n credentials,\n }\n\n const session = await adapter.createSession(sessionInput)\n\n const transaction = em.create(GatewayTransaction, {\n paymentId: input.paymentId,\n providerKey: input.providerKey,\n providerSessionId: session.sessionId,\n unifiedStatus: session.status,\n redirectUrl: session.redirectUrl\n ?? (session.clientSession?.type === 'redirect' ? session.clientSession.redirectUrl : null),\n clientSecret: session.clientSecret ?? null,\n amount: String(input.amount),\n currencyCode: input.currencyCode,\n gatewayMetadata: {\n ...(session.providerData ?? {}),\n ...(session.clientSession ? { clientSession: session.clientSession } : {}),\n },\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n })\n await em.persist(transaction).flush()\n await emitPaymentGatewayEvent('payment_gateways.session.created', {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n status: transaction.unifiedStatus,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n await writeTransactionLog(\n transaction.providerKey,\n scope,\n transaction.id,\n 'info',\n 'Payment session created',\n {\n paymentId: transaction.paymentId,\n providerSessionId: transaction.providerSessionId,\n status: transaction.unifiedStatus,\n amount: input.amount,\n currencyCode: input.currencyCode,\n },\n )\n\n return { transaction, session }\n },\n\n async capturePayment(transactionId: string, amount: number | undefined, scope: { organizationId: string; tenantId: string }): Promise<CaptureResult> {\n const transaction = await findTransactionOrThrow(transactionId, scope)\n assertManualActionAllowed('capture', transaction)\n const { adapter, credentials } = await resolveAdapterAndCredentials(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n )\n\n const result = await adapter.capture({\n sessionId: readProviderSessionId(transaction),\n amount,\n credentials,\n })\n\n const statusChanged = applyAdapterResultStatus('capture', transaction, result.status)\n transaction.gatewayMetadata = { ...readGatewayMetadata(transaction.gatewayMetadata), captureResult: result.providerData }\n await em.flush()\n if (statusChanged) {\n await emitStatusEvent(result.status, {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n }\n await writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'info',\n 'Payment captured',\n {\n amount: amount ?? null,\n status: result.status,\n capturedAmount: result.capturedAmount,\n },\n )\n\n return result\n },\n\n async refundPayment(\n transactionId: string,\n amount: number | undefined,\n reason: string | undefined,\n scope: { organizationId: string; tenantId: string },\n ): Promise<RefundResult> {\n const transaction = await findTransactionOrThrow(transactionId, scope)\n assertManualActionAllowed('refund', transaction)\n const { adapter, credentials } = await resolveAdapterAndCredentials(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n )\n\n const result = await adapter.refund({\n sessionId: readProviderSessionId(transaction),\n amount,\n reason,\n credentials,\n })\n\n const statusChanged = applyAdapterResultStatus('refund', transaction, result.status)\n transaction.gatewayRefundId = result.refundId\n transaction.gatewayMetadata = { ...readGatewayMetadata(transaction.gatewayMetadata), refundResult: result.providerData }\n await em.flush()\n if (statusChanged) {\n await emitStatusEvent(result.status, {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n }\n await writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'info',\n 'Payment refunded',\n {\n amount: amount ?? null,\n reason: reason ?? null,\n status: result.status,\n refundId: result.refundId,\n },\n )\n\n return result\n },\n\n async cancelPayment(\n transactionId: string,\n reason: string | undefined,\n scope: { organizationId: string; tenantId: string },\n ): Promise<CancelResult> {\n const transaction = await findTransactionOrThrow(transactionId, scope)\n assertManualActionAllowed('cancel', transaction)\n const { adapter, credentials } = await resolveAdapterAndCredentials(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n )\n\n const result = await adapter.cancel({\n sessionId: readProviderSessionId(transaction),\n reason,\n credentials,\n })\n\n const statusChanged = applyAdapterResultStatus('cancel', transaction, result.status)\n await em.flush()\n if (statusChanged) {\n await emitStatusEvent(result.status, {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n }\n await writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'info',\n 'Payment cancelled',\n {\n reason: reason ?? null,\n status: result.status,\n },\n )\n\n return result\n },\n\n async getPaymentStatus(transactionId: string, scope: { organizationId: string; tenantId: string }): Promise<GatewayPaymentStatus> {\n const transaction = await findTransactionOrThrow(transactionId, scope)\n const { adapter, credentials } = await resolveAdapterAndCredentials(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n )\n\n const status = await adapter.getStatus({\n sessionId: readProviderSessionId(transaction),\n credentials,\n })\n\n if (status.status !== transaction.unifiedStatus && isValidTransition(transaction.unifiedStatus as UnifiedPaymentStatus, status.status)) {\n const previousStatus = transaction.unifiedStatus\n transaction.unifiedStatus = status.status\n transaction.gatewayStatus = status.status\n transaction.gatewayMetadata = { ...readGatewayMetadata(transaction.gatewayMetadata), statusResult: status.providerData ?? null }\n transaction.lastPolledAt = new Date()\n await em.flush()\n await emitStatusEvent(status.status, {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n previousStatus,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n await writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'info',\n 'Payment status updated by poller',\n {\n previousStatus,\n nextStatus: status.status,\n },\n )\n }\n\n return status\n },\n\n async syncTransactionStatus(transactionId: string, update: {\n unifiedStatus: UnifiedPaymentStatus\n providerStatus?: string\n providerData?: Record<string, unknown>\n webhookEvent?: {\n eventType: string\n idempotencyKey: string\n processed: boolean\n receivedAt?: string\n }\n }, scope: { organizationId: string; tenantId: string }): Promise<void> {\n const transaction = await findTransactionOrThrow(transactionId, scope)\n const currentStatus = transaction.unifiedStatus as UnifiedPaymentStatus\n const canTransition = isValidTransition(currentStatus, update.unifiedStatus)\n const shouldApplyStatus = canTransition && update.unifiedStatus !== currentStatus\n const previousStatus = transaction.unifiedStatus\n if (shouldApplyStatus) {\n transaction.unifiedStatus = update.unifiedStatus\n }\n if (update.providerStatus) {\n transaction.gatewayStatus = update.providerStatus\n }\n if (update.providerData) {\n transaction.gatewayMetadata = { ...readGatewayMetadata(transaction.gatewayMetadata), ...update.providerData }\n }\n if (update.webhookEvent) {\n const webhookLog = readWebhookLog(transaction.webhookLog)\n webhookLog.push({\n eventType: update.webhookEvent.eventType,\n receivedAt: update.webhookEvent.receivedAt ?? new Date().toISOString(),\n idempotencyKey: update.webhookEvent.idempotencyKey,\n unifiedStatus: update.unifiedStatus,\n processed: update.webhookEvent.processed,\n })\n transaction.webhookLog = webhookLog\n }\n transaction.lastWebhookAt = new Date()\n await em.flush()\n if (shouldApplyStatus) {\n await emitStatusEvent(update.unifiedStatus, {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n previousStatus,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n }\n await writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n shouldApplyStatus ? 'info' : 'warn',\n shouldApplyStatus ? 'Payment status synchronized from webhook' : 'Webhook received with no status transition',\n {\n previousStatus,\n nextStatus: update.unifiedStatus,\n providerStatus: update.providerStatus ?? null,\n eventType: update.webhookEvent?.eventType ?? null,\n idempotencyKey: update.webhookEvent?.idempotencyKey ?? null,\n },\n )\n },\n\n async findTransaction(id: string, scope: { organizationId: string; tenantId: string }): Promise<GatewayTransaction | null> {\n return findOneWithDecryption(\n em,\n GatewayTransaction,\n {\n id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n },\n\n async findTransactionBySessionId(\n providerSessionId: string,\n scope: { organizationId: string; tenantId: string },\n providerKey?: string,\n ): Promise<GatewayTransaction | null> {\n return findOneWithDecryption(\n em,\n GatewayTransaction,\n {\n providerSessionId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n ...(providerKey ? { providerKey } : {}),\n },\n undefined,\n scope,\n )\n },\n\n async listTransactionsForStatusPolling(scope?: {\n organizationId?: string\n tenantId?: string\n providerKey?: string\n limit?: number\n }): Promise<GatewayTransaction[]> {\n const where: Record<string, unknown> = {\n unifiedStatus: { $in: ['pending', 'authorized', 'partially_captured'] },\n deletedAt: null,\n }\n if (scope?.organizationId) where.organizationId = scope.organizationId\n if (scope?.tenantId) where.tenantId = scope.tenantId\n if (scope?.providerKey) where.providerKey = scope.providerKey\n\n return findWithDecryption(\n em,\n GatewayTransaction,\n where,\n {\n orderBy: { updatedAt: 'asc' },\n limit: scope?.limit ?? 100,\n },\n scope,\n )\n },\n }\n}\n\nexport type PaymentGatewayService = ReturnType<typeof createPaymentGatewayService>\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { parseDecryptedFieldValue } from "@open-mercato/shared/lib/encryption/tenantDataEncryptionService";
|
|
2
|
+
function normalizeDecryptedValue(value) {
|
|
3
|
+
return typeof value === "string" ? parseDecryptedFieldValue(value) : value;
|
|
4
|
+
}
|
|
5
|
+
function readGatewayMetadata(value) {
|
|
6
|
+
const parsed = normalizeDecryptedValue(value);
|
|
7
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
8
|
+
}
|
|
9
|
+
function readWebhookLog(value) {
|
|
10
|
+
const parsed = normalizeDecryptedValue(value);
|
|
11
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
readGatewayMetadata,
|
|
15
|
+
readWebhookLog
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=transaction-fields.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/payment_gateways/lib/transaction-fields.ts"],
|
|
4
|
+
"sourcesContent": ["import { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport type { GatewayTransaction } from '../data/entities'\n\nexport type WebhookLogEntry = NonNullable<GatewayTransaction['webhookLog']>[number]\n\n// The tenant encryption layer encrypts gateway_metadata and webhook_log as JSON-serialized blobs,\n// but decryption returns the raw decrypted string (entity fields are never auto-parsed to objects \u2014\n// issue #1810). These helpers restore the structured shape consumers expect, mirroring the\n// normalization used by messages (resolveMessageActionData) and integrations (normalizeCredentials).\nfunction normalizeDecryptedValue(value: unknown): unknown {\n return typeof value === 'string' ? parseDecryptedFieldValue(value) : value\n}\n\nexport function readGatewayMetadata(value: unknown): Record<string, unknown> {\n const parsed = normalizeDecryptedValue(value)\n return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {}\n}\n\nexport function readWebhookLog(value: unknown): WebhookLogEntry[] {\n const parsed = normalizeDecryptedValue(value)\n return Array.isArray(parsed) ? (parsed as WebhookLogEntry[]) : []\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,gCAAgC;AASzC,SAAS,wBAAwB,OAAyB;AACxD,SAAO,OAAO,UAAU,WAAW,yBAAyB,KAAK,IAAI;AACvE;AAEO,SAAS,oBAAoB,OAAyC;AAC3E,QAAM,SAAS,wBAAwB,KAAK;AAC5C,SAAO,WAAW,QAAQ,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,IACxE,SACD,CAAC;AACP;AAEO,SAAS,eAAe,OAAmC;AAChE,QAAM,SAAS,wBAAwB,KAAK;AAC5C,SAAO,MAAM,QAAQ,MAAM,IAAK,SAA+B,CAAC;AAClE;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6374.1.49e692558f",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -246,16 +246,16 @@
|
|
|
246
246
|
"zod": "^4.4.3"
|
|
247
247
|
},
|
|
248
248
|
"peerDependencies": {
|
|
249
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
250
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
251
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
249
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6374.1.49e692558f",
|
|
250
|
+
"@open-mercato/shared": "0.6.6-develop.6374.1.49e692558f",
|
|
251
|
+
"@open-mercato/ui": "0.6.6-develop.6374.1.49e692558f",
|
|
252
252
|
"react": "^19.0.0",
|
|
253
253
|
"react-dom": "^19.0.0"
|
|
254
254
|
},
|
|
255
255
|
"devDependencies": {
|
|
256
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
256
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6374.1.49e692558f",
|
|
257
|
+
"@open-mercato/shared": "0.6.6-develop.6374.1.49e692558f",
|
|
258
|
+
"@open-mercato/ui": "0.6.6-develop.6374.1.49e692558f",
|
|
259
259
|
"@testing-library/dom": "^10.4.1",
|
|
260
260
|
"@testing-library/jest-dom": "^6.9.1",
|
|
261
261
|
"@testing-library/react": "^16.3.1",
|
|
@@ -13,7 +13,7 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
|
13
13
|
import { booleanOverrideSelectValue } from "./overrideFormConfig";
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
export function
|
|
16
|
+
export function DefaultValueField(props: CrudCustomFieldRenderProps) {
|
|
17
17
|
const t = useT()
|
|
18
18
|
const selectedType = props.values?.type as string;
|
|
19
19
|
|
|
@@ -132,7 +132,7 @@ export function createFieldDefinitions(
|
|
|
132
132
|
id: 'defaultValue',
|
|
133
133
|
label: '',
|
|
134
134
|
type: 'custom',
|
|
135
|
-
component:
|
|
135
|
+
component: (props) => <DefaultValueField {...props} />,
|
|
136
136
|
description: t('feature_toggles.form.fields.defaultValue.description'),
|
|
137
137
|
},
|
|
138
138
|
]
|
|
@@ -20,7 +20,7 @@ export function booleanOverrideSelectValue(value: unknown): 'true' | 'false' {
|
|
|
20
20
|
return value === true || value === 'true' ? 'true' : 'false'
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export function
|
|
23
|
+
export function OverrideValueField(props: CrudCustomFieldRenderProps) {
|
|
24
24
|
const t = useT()
|
|
25
25
|
const toggleType = props.values?.toggleType as string;
|
|
26
26
|
const isOverride = props.values?.isOverride as boolean;
|
|
@@ -119,7 +119,7 @@ export function createOverrideFieldDefinitions(
|
|
|
119
119
|
id: 'overrideValue',
|
|
120
120
|
label: '',
|
|
121
121
|
type: 'custom',
|
|
122
|
-
component:
|
|
122
|
+
component: (props) => <OverrideValueField {...props} />,
|
|
123
123
|
description: t('feature_toggles.override.fields.overrideValue.description'),
|
|
124
124
|
},
|
|
125
125
|
]
|
|
@@ -324,6 +324,29 @@ export function ActionCard({
|
|
|
324
324
|
)
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
if (action.status !== 'pending') {
|
|
328
|
+
const isProcessing = action.status === 'processing'
|
|
329
|
+
const statusLabel = action.status === 'accepted'
|
|
330
|
+
? t('inbox_ops.status.accepted', 'Accepted')
|
|
331
|
+
: isProcessing
|
|
332
|
+
? t('inbox_ops.status.processing', 'Processing')
|
|
333
|
+
: action.status
|
|
334
|
+
return (
|
|
335
|
+
<div className="border rounded-lg p-3 md:p-4 bg-muted/50">
|
|
336
|
+
<div className="flex items-center gap-2 mb-2">
|
|
337
|
+
{isProcessing ? (
|
|
338
|
+
<RefreshCw className="h-5 w-5 text-muted-foreground flex-shrink-0 animate-spin" />
|
|
339
|
+
) : (
|
|
340
|
+
<CheckCircle className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
|
341
|
+
)}
|
|
342
|
+
<span className="text-sm font-medium">{label}</span>
|
|
343
|
+
<span className="text-xs text-muted-foreground">{statusLabel}</span>
|
|
344
|
+
</div>
|
|
345
|
+
<p className="text-sm text-muted-foreground">{displayDescription}</p>
|
|
346
|
+
</div>
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
|
|
327
350
|
const hasNameIssue = hasContactNameIssue(action)
|
|
328
351
|
|
|
329
352
|
return (
|
|
@@ -13,7 +13,7 @@ import { toErrorMessage } from './message-detail/utils'
|
|
|
13
13
|
|
|
14
14
|
export type MessageFolder = 'inbox' | 'sent' | 'drafts' | 'archived' | 'all'
|
|
15
15
|
|
|
16
|
-
export type MessageBulkActionId = 'markRead' | 'markUnread' | 'archive' | 'delete'
|
|
16
|
+
export type MessageBulkActionId = 'markRead' | 'markUnread' | 'archive' | 'unarchive' | 'delete'
|
|
17
17
|
|
|
18
18
|
type BulkExecutionSummary = {
|
|
19
19
|
action: MessageBulkActionId
|
|
@@ -84,6 +84,14 @@ const MESSAGE_BULK_REQUESTS: Record<MessageBulkActionId, MessageBulkRequestConfi
|
|
|
84
84
|
errorKey: 'messages.errors.stateChangeFailed',
|
|
85
85
|
errorFallback: 'Failed to update message state.',
|
|
86
86
|
},
|
|
87
|
+
unarchive: {
|
|
88
|
+
method: 'DELETE',
|
|
89
|
+
buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/archive`,
|
|
90
|
+
successKey: 'messages.bulk.flash.unarchiveSuccess',
|
|
91
|
+
successFallback: '{count} messages moved to inbox.',
|
|
92
|
+
errorKey: 'messages.errors.stateChangeFailed',
|
|
93
|
+
errorFallback: 'Failed to update message state.',
|
|
94
|
+
},
|
|
87
95
|
delete: {
|
|
88
96
|
method: 'DELETE',
|
|
89
97
|
buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}`,
|
|
@@ -110,6 +118,11 @@ const MESSAGE_BULK_ACTIONS: Record<MessageBulkActionId, MessageBulkActionDefinit
|
|
|
110
118
|
labelKey: 'messages.actions.archive',
|
|
111
119
|
labelFallback: 'Archive',
|
|
112
120
|
},
|
|
121
|
+
unarchive: {
|
|
122
|
+
id: 'messages-unarchive',
|
|
123
|
+
labelKey: 'messages.actions.unarchive',
|
|
124
|
+
labelFallback: 'Unarchive',
|
|
125
|
+
},
|
|
113
126
|
delete: {
|
|
114
127
|
id: 'messages-delete',
|
|
115
128
|
labelKey: 'messages.actions.delete',
|
|
@@ -120,7 +133,7 @@ const MESSAGE_BULK_ACTIONS: Record<MessageBulkActionId, MessageBulkActionDefinit
|
|
|
120
133
|
|
|
121
134
|
const MESSAGE_BULK_ACTION_IDS_BY_FOLDER = {
|
|
122
135
|
inbox: ['markRead', 'markUnread', 'archive', 'delete'],
|
|
123
|
-
archived: ['markRead', 'markUnread', 'delete'],
|
|
136
|
+
archived: ['markRead', 'markUnread', 'unarchive', 'delete'],
|
|
124
137
|
sent: ['delete'],
|
|
125
138
|
drafts: ['delete'],
|
|
126
139
|
all: [],
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"messages.bulk.flash.markReadSuccess": "{count} Nachrichten als gelesen markiert.",
|
|
35
35
|
"messages.bulk.flash.markUnreadSuccess": "{count} Nachrichten als ungelesen markiert.",
|
|
36
36
|
"messages.bulk.flash.partial": "{succeeded} von {total} Nachrichten verarbeitet; {failed} fehlgeschlagen.",
|
|
37
|
+
"messages.bulk.flash.unarchiveSuccess": "{count} Nachrichten in den Posteingang verschoben.",
|
|
37
38
|
"messages.compose": "Nachricht verfassen",
|
|
38
39
|
"messages.composeHint": "Erstelle eine Nachricht, hänge Objekte an und sende sie an ausgewählte Empfänger.",
|
|
39
40
|
"messages.composer.attachmentPicker.confirm": "Auswahl anhängen",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"messages.bulk.flash.markReadSuccess": "{count} messages marked as read.",
|
|
35
35
|
"messages.bulk.flash.markUnreadSuccess": "{count} messages marked as unread.",
|
|
36
36
|
"messages.bulk.flash.partial": "{succeeded} of {total} messages processed; {failed} failed.",
|
|
37
|
+
"messages.bulk.flash.unarchiveSuccess": "{count} messages moved to inbox.",
|
|
37
38
|
"messages.compose": "Compose message",
|
|
38
39
|
"messages.composeHint": "Create a message, attach objects, and send it to selected recipients.",
|
|
39
40
|
"messages.composer.attachmentPicker.confirm": "Attach selected",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"messages.bulk.flash.markReadSuccess": "{count} mensajes marcados como leídos.",
|
|
35
35
|
"messages.bulk.flash.markUnreadSuccess": "{count} mensajes marcados como no leídos.",
|
|
36
36
|
"messages.bulk.flash.partial": "Se procesaron {succeeded} de {total} mensajes; {failed} fallaron.",
|
|
37
|
+
"messages.bulk.flash.unarchiveSuccess": "{count} mensajes movidos a la bandeja de entrada.",
|
|
37
38
|
"messages.compose": "Redactar mensaje",
|
|
38
39
|
"messages.composeHint": "Crea un mensaje, adjunta objetos y envíalo a los destinatarios seleccionados.",
|
|
39
40
|
"messages.composer.attachmentPicker.confirm": "Adjuntar seleccionados",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"messages.bulk.flash.markReadSuccess": "Oznaczono {count} wiadomości jako przeczytane.",
|
|
35
35
|
"messages.bulk.flash.markUnreadSuccess": "Oznaczono {count} wiadomości jako nieprzeczytane.",
|
|
36
36
|
"messages.bulk.flash.partial": "Przetworzono {succeeded} z {total} wiadomości; niepowodzeń: {failed}.",
|
|
37
|
+
"messages.bulk.flash.unarchiveSuccess": "Przeniesiono {count} wiadomości do skrzynki odbiorczej.",
|
|
37
38
|
"messages.compose": "Napisz wiadomość",
|
|
38
39
|
"messages.composeHint": "Utwórz wiadomość, dołącz obiekty i wyślij do wybranych odbiorców.",
|
|
39
40
|
"messages.composer.attachmentPicker.confirm": "Dołącz wybrane",
|
|
@@ -5,6 +5,7 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
|
5
5
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
6
6
|
import type { IntegrationLogService } from '../../../../integrations/lib/log-service'
|
|
7
7
|
import { GatewayTransaction } from '../../../data/entities'
|
|
8
|
+
import { readGatewayMetadata, readWebhookLog } from '../../../lib/transaction-fields'
|
|
8
9
|
import { paymentGatewaysTag } from '../../openapi'
|
|
9
10
|
|
|
10
11
|
export const metadata = {
|
|
@@ -82,8 +83,8 @@ export async function GET(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
82
83
|
redirectUrl: transaction.redirectUrl ?? null,
|
|
83
84
|
amount: transaction.amount,
|
|
84
85
|
currencyCode: transaction.currencyCode,
|
|
85
|
-
gatewayMetadata: transaction.gatewayMetadata
|
|
86
|
-
webhookLog:
|
|
86
|
+
gatewayMetadata: transaction.gatewayMetadata == null ? null : readGatewayMetadata(transaction.gatewayMetadata),
|
|
87
|
+
webhookLog: readWebhookLog(transaction.webhookLog),
|
|
87
88
|
lastWebhookAt: toIsoString(transaction.lastWebhookAt),
|
|
88
89
|
lastPolledAt: toIsoString(transaction.lastPolledAt),
|
|
89
90
|
expiresAt: toIsoString(transaction.expiresAt),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'
|
|
2
|
+
|
|
3
|
+
// Gateway transactions persist provider secrets and raw provider payloads. Encrypt the client
|
|
4
|
+
// secret plus the JSONB columns that carry provider/session data and webhook history at rest.
|
|
5
|
+
// Lookup-critical columns (provider_key, provider_session_id, unified_status, gateway_payment_id,
|
|
6
|
+
// gateway_refund_id) are intentionally left in plaintext so status filters and session lookups keep
|
|
7
|
+
// working without a deterministic hash. The JSONB fields decrypt back to strings (entity fields are
|
|
8
|
+
// never auto-parsed — see issue #1810), so consumers MUST normalize them via lib/transaction-fields.
|
|
9
|
+
export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
|
|
10
|
+
{
|
|
11
|
+
entityId: 'payment_gateways:gateway_transaction',
|
|
12
|
+
fields: [
|
|
13
|
+
{ field: 'client_secret' },
|
|
14
|
+
{ field: 'gateway_metadata' },
|
|
15
|
+
{ field: 'webhook_log' },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
export default defaultEncryptionMaps
|
|
@@ -18,6 +18,7 @@ import { conflict } from '@open-mercato/shared/lib/crud/errors'
|
|
|
18
18
|
import { GatewayTransaction } from '../data/entities'
|
|
19
19
|
import { canApplyManualAction, isValidTransition, type ManualGatewayAction } from './status-machine'
|
|
20
20
|
import { emitPaymentGatewayEvent } from '../events'
|
|
21
|
+
import { readGatewayMetadata, readWebhookLog } from './transaction-fields'
|
|
21
22
|
|
|
22
23
|
function assertManualActionAllowed(action: ManualGatewayAction, transaction: GatewayTransaction): void {
|
|
23
24
|
const current = transaction.unifiedStatus as UnifiedPaymentStatus
|
|
@@ -234,7 +235,7 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
|
|
|
234
235
|
})
|
|
235
236
|
|
|
236
237
|
const statusChanged = applyAdapterResultStatus('capture', transaction, result.status)
|
|
237
|
-
transaction.gatewayMetadata = { ...transaction.gatewayMetadata, captureResult: result.providerData }
|
|
238
|
+
transaction.gatewayMetadata = { ...readGatewayMetadata(transaction.gatewayMetadata), captureResult: result.providerData }
|
|
238
239
|
await em.flush()
|
|
239
240
|
if (statusChanged) {
|
|
240
241
|
await emitStatusEvent(result.status, {
|
|
@@ -283,7 +284,7 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
|
|
|
283
284
|
|
|
284
285
|
const statusChanged = applyAdapterResultStatus('refund', transaction, result.status)
|
|
285
286
|
transaction.gatewayRefundId = result.refundId
|
|
286
|
-
transaction.gatewayMetadata = { ...transaction.gatewayMetadata, refundResult: result.providerData }
|
|
287
|
+
transaction.gatewayMetadata = { ...readGatewayMetadata(transaction.gatewayMetadata), refundResult: result.providerData }
|
|
287
288
|
await em.flush()
|
|
288
289
|
if (statusChanged) {
|
|
289
290
|
await emitStatusEvent(result.status, {
|
|
@@ -371,7 +372,7 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
|
|
|
371
372
|
const previousStatus = transaction.unifiedStatus
|
|
372
373
|
transaction.unifiedStatus = status.status
|
|
373
374
|
transaction.gatewayStatus = status.status
|
|
374
|
-
transaction.gatewayMetadata = { ...transaction.gatewayMetadata, statusResult: status.providerData ?? null }
|
|
375
|
+
transaction.gatewayMetadata = { ...readGatewayMetadata(transaction.gatewayMetadata), statusResult: status.providerData ?? null }
|
|
375
376
|
transaction.lastPolledAt = new Date()
|
|
376
377
|
await em.flush()
|
|
377
378
|
await emitStatusEvent(status.status, {
|
|
@@ -421,10 +422,10 @@ export function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {
|
|
|
421
422
|
transaction.gatewayStatus = update.providerStatus
|
|
422
423
|
}
|
|
423
424
|
if (update.providerData) {
|
|
424
|
-
transaction.gatewayMetadata = { ...transaction.gatewayMetadata, ...update.providerData }
|
|
425
|
+
transaction.gatewayMetadata = { ...readGatewayMetadata(transaction.gatewayMetadata), ...update.providerData }
|
|
425
426
|
}
|
|
426
427
|
if (update.webhookEvent) {
|
|
427
|
-
const webhookLog =
|
|
428
|
+
const webhookLog = readWebhookLog(transaction.webhookLog)
|
|
428
429
|
webhookLog.push({
|
|
429
430
|
eventType: update.webhookEvent.eventType,
|
|
430
431
|
receivedAt: update.webhookEvent.receivedAt ?? new Date().toISOString(),
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'
|
|
2
|
+
import type { GatewayTransaction } from '../data/entities'
|
|
3
|
+
|
|
4
|
+
export type WebhookLogEntry = NonNullable<GatewayTransaction['webhookLog']>[number]
|
|
5
|
+
|
|
6
|
+
// The tenant encryption layer encrypts gateway_metadata and webhook_log as JSON-serialized blobs,
|
|
7
|
+
// but decryption returns the raw decrypted string (entity fields are never auto-parsed to objects —
|
|
8
|
+
// issue #1810). These helpers restore the structured shape consumers expect, mirroring the
|
|
9
|
+
// normalization used by messages (resolveMessageActionData) and integrations (normalizeCredentials).
|
|
10
|
+
function normalizeDecryptedValue(value: unknown): unknown {
|
|
11
|
+
return typeof value === 'string' ? parseDecryptedFieldValue(value) : value
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function readGatewayMetadata(value: unknown): Record<string, unknown> {
|
|
15
|
+
const parsed = normalizeDecryptedValue(value)
|
|
16
|
+
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
17
|
+
? (parsed as Record<string, unknown>)
|
|
18
|
+
: {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function readWebhookLog(value: unknown): WebhookLogEntry[] {
|
|
22
|
+
const parsed = normalizeDecryptedValue(value)
|
|
23
|
+
return Array.isArray(parsed) ? (parsed as WebhookLogEntry[]) : []
|
|
24
|
+
}
|