@open-mercato/core 0.4.8-develop-15259be22b → 0.4.8-develop-280c02b529
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/dist/generated/entities/inbox_proposal/index.js +2 -0
- package/dist/generated/entities/inbox_proposal/index.js.map +2 -2
- package/dist/modules/catalog/inbox-actions.js +49 -0
- package/dist/modules/catalog/inbox-actions.js.map +2 -2
- package/dist/modules/customers/inbox-actions.js +69 -27
- package/dist/modules/customers/inbox-actions.js.map +3 -3
- package/dist/modules/inbox_ops/ai-tools.js +346 -0
- package/dist/modules/inbox_ops/ai-tools.js.map +7 -0
- package/dist/modules/inbox_ops/api/extract/route.js +3 -2
- package/dist/modules/inbox_ops/api/extract/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js +4 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/accept/route.js +4 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/accept/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/complete/route.js +4 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/complete/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/reject/route.js +4 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/reject/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +4 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js +59 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js.map +7 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/reject/route.js +4 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/reject/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.js +34 -14
- package/dist/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/counts/route.js +49 -4
- package/dist/modules/inbox_ops/api/proposals/counts/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/route.js +13 -0
- package/dist/modules/inbox_ops/api/proposals/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/settings/route.js +33 -2
- package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js +28 -3
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js +103 -5
- package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js.map +2 -2
- package/dist/modules/inbox_ops/components/messages/InboxEmailContent.js +24 -0
- package/dist/modules/inbox_ops/components/messages/InboxEmailContent.js.map +7 -0
- package/dist/modules/inbox_ops/components/messages/InboxEmailPreview.js +29 -0
- package/dist/modules/inbox_ops/components/messages/InboxEmailPreview.js.map +7 -0
- package/dist/modules/inbox_ops/components/proposals/CategoryBadge.js +59 -0
- package/dist/modules/inbox_ops/components/proposals/CategoryBadge.js.map +7 -0
- package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js +3 -1
- package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js.map +2 -2
- package/dist/modules/inbox_ops/data/entities.js +4 -0
- package/dist/modules/inbox_ops/data/entities.js.map +2 -2
- package/dist/modules/inbox_ops/data/validators.js +30 -5
- package/dist/modules/inbox_ops/data/validators.js.map +2 -2
- package/dist/modules/inbox_ops/lib/cache.js +53 -0
- package/dist/modules/inbox_ops/lib/cache.js.map +7 -0
- package/dist/modules/inbox_ops/lib/contactValidation.js +38 -3
- package/dist/modules/inbox_ops/lib/contactValidation.js.map +2 -2
- package/dist/modules/inbox_ops/lib/executionHelpers.js +28 -1
- package/dist/modules/inbox_ops/lib/executionHelpers.js.map +2 -2
- package/dist/modules/inbox_ops/lib/extractionPrompt.js +2 -1
- package/dist/modules/inbox_ops/lib/extractionPrompt.js.map +2 -2
- package/dist/modules/inbox_ops/lib/messageObjectPreviews.js +52 -0
- package/dist/modules/inbox_ops/lib/messageObjectPreviews.js.map +7 -0
- package/dist/modules/inbox_ops/lib/messagesIntegration.js +155 -0
- package/dist/modules/inbox_ops/lib/messagesIntegration.js.map +7 -0
- package/dist/modules/inbox_ops/message-objects.js +36 -0
- package/dist/modules/inbox_ops/message-objects.js.map +7 -0
- package/dist/modules/inbox_ops/message-types.js +38 -0
- package/dist/modules/inbox_ops/message-types.js.map +7 -0
- package/dist/modules/inbox_ops/migrations/Migration20260303173020.js +13 -0
- package/dist/modules/inbox_ops/migrations/Migration20260303173020.js.map +7 -0
- package/dist/modules/inbox_ops/migrations/Migration20260303173215.js +15 -0
- package/dist/modules/inbox_ops/migrations/Migration20260303173215.js.map +7 -0
- package/dist/modules/inbox_ops/search.js +5 -3
- package/dist/modules/inbox_ops/search.js.map +2 -2
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js +65 -3
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +2 -2
- package/generated/entities/inbox_proposal/index.ts +1 -0
- package/package.json +3 -3
- package/src/modules/catalog/inbox-actions.ts +55 -0
- package/src/modules/customers/inbox-actions.ts +86 -27
- package/src/modules/inbox_ops/ai-tools.ts +451 -0
- package/src/modules/inbox_ops/api/extract/route.ts +3 -2
- package/src/modules/inbox_ops/api/proposals/[id]/accept-all/route.ts +5 -0
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/accept/route.ts +5 -0
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/complete/route.ts +5 -0
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/reject/route.ts +5 -0
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +5 -0
- package/src/modules/inbox_ops/api/proposals/[id]/categorize/route.ts +61 -0
- package/src/modules/inbox_ops/api/proposals/[id]/reject/route.ts +5 -0
- package/src/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.ts +36 -16
- package/src/modules/inbox_ops/api/proposals/counts/route.ts +60 -5
- package/src/modules/inbox_ops/api/proposals/route.ts +14 -1
- package/src/modules/inbox_ops/api/settings/route.ts +36 -2
- package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +31 -3
- package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.tsx +103 -1
- package/src/modules/inbox_ops/components/messages/InboxEmailContent.tsx +45 -0
- package/src/modules/inbox_ops/components/messages/InboxEmailPreview.tsx +40 -0
- package/src/modules/inbox_ops/components/proposals/CategoryBadge.tsx +59 -0
- package/src/modules/inbox_ops/components/proposals/EditActionDialog.tsx +3 -1
- package/src/modules/inbox_ops/components/proposals/types.ts +1 -0
- package/src/modules/inbox_ops/data/entities.ts +14 -1
- package/src/modules/inbox_ops/data/validators.ts +41 -5
- package/src/modules/inbox_ops/lib/cache.ts +60 -0
- package/src/modules/inbox_ops/lib/contactValidation.ts +31 -2
- package/src/modules/inbox_ops/lib/executionHelpers.ts +40 -0
- package/src/modules/inbox_ops/lib/extractionPrompt.ts +2 -1
- package/src/modules/inbox_ops/lib/messageObjectPreviews.ts +61 -0
- package/src/modules/inbox_ops/lib/messagesIntegration.ts +231 -0
- package/src/modules/inbox_ops/message-objects.ts +34 -0
- package/src/modules/inbox_ops/message-types.ts +36 -0
- package/src/modules/inbox_ops/migrations/Migration20260303173020.ts +13 -0
- package/src/modules/inbox_ops/migrations/Migration20260303173215.ts +15 -0
- package/src/modules/inbox_ops/search.ts +5 -3
- package/src/modules/inbox_ops/subscribers/extractionWorker.ts +75 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../../src/modules/inbox_ops/api/proposals/%5Bid%5D/actions/%5BactionId%5D/reject/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rejectAction } from '../../../../../../lib/executionEngine'\nimport {\n resolveRequestContext,\n resolveActionAndProposal,\n toExecutionContext,\n handleRouteError,\n isErrorResponse,\n} from '../../../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const resolved = await resolveActionAndProposal(new URL(req.url), ctx)\n if (isErrorResponse(resolved)) return resolved\n\n if (resolved.action.status !== 'pending' && resolved.action.status !== 'failed') {\n return NextResponse.json({ error: 'Action already processed' }, { status: 409 })\n }\n\n await rejectAction(resolved.action, toExecutionContext(ctx))\n\n return NextResponse.json({ ok: true })\n } catch (err) {\n return handleRouteError(err, 'reject action')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Reject action',\n methods: {\n POST: {\n summary: 'Reject a proposal action',\n responses: [\n { status: 200, description: 'Action rejected' },\n { status: 404, description: 'Action not found' },\n { status: 409, description: 'Action already processed' },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAC3C,UAAM,WAAW,MAAM,yBAAyB,IAAI,IAAI,IAAI,GAAG,GAAG,GAAG;AACrE,QAAI,gBAAgB,QAAQ,EAAG,QAAO;AAEtC,QAAI,SAAS,OAAO,WAAW,aAAa,SAAS,OAAO,WAAW,UAAU;AAC/E,aAAO,aAAa,KAAK,EAAE,OAAO,2BAA2B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACjF;AAEA,UAAM,aAAa,SAAS,QAAQ,mBAAmB,GAAG,CAAC;AAE3D,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC,SAAS,KAAK;AACZ,WAAO,iBAAiB,KAAK,eAAe;AAAA,EAC9C;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,kBAAkB;AAAA,QAC9C,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,QAC/C,EAAE,QAAQ,KAAK,aAAa,2BAA2B;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AACF;",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { rejectAction } from '../../../../../../lib/executionEngine'\nimport { resolveCache, invalidateCountsCache } from '../../../../../../lib/cache'\nimport {\n resolveRequestContext,\n resolveActionAndProposal,\n toExecutionContext,\n handleRouteError,\n isErrorResponse,\n} from '../../../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const resolved = await resolveActionAndProposal(new URL(req.url), ctx)\n if (isErrorResponse(resolved)) return resolved\n\n if (resolved.action.status !== 'pending' && resolved.action.status !== 'failed') {\n return NextResponse.json({ error: 'Action already processed' }, { status: 409 })\n }\n\n await rejectAction(resolved.action, toExecutionContext(ctx))\n\n const cache = resolveCache(ctx.container)\n await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))\n\n return NextResponse.json({ ok: true })\n } catch (err) {\n return handleRouteError(err, 'reject action')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Reject action',\n methods: {\n POST: {\n summary: 'Reject a proposal action',\n responses: [\n { status: 200, description: 'Action rejected' },\n { status: 404, description: 'Action not found' },\n { status: 409, description: 'Action already processed' },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAC7B,SAAS,cAAc,6BAA6B;AACpD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAC3C,UAAM,WAAW,MAAM,yBAAyB,IAAI,IAAI,IAAI,GAAG,GAAG,GAAG;AACrE,QAAI,gBAAgB,QAAQ,EAAG,QAAO;AAEtC,QAAI,SAAS,OAAO,WAAW,aAAa,SAAS,OAAO,WAAW,UAAU;AAC/E,aAAO,aAAa,KAAK,EAAE,OAAO,2BAA2B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACjF;AAEA,UAAM,aAAa,SAAS,QAAQ,mBAAmB,GAAG,CAAC;AAE3D,UAAM,QAAQ,aAAa,IAAI,SAAS;AACxC,UAAM,mBAAmB,IAAI,UAAU,MAAM,sBAAsB,OAAO,IAAI,QAAQ,CAAC;AAEvF,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC,SAAS,KAAK;AACZ,WAAO,iBAAiB,KAAK,eAAe;AAAA,EAC9C;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,kBAAkB;AAAA,QAC9C,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,QAC/C,EAAE,QAAQ,KAAK,aAAa,2BAA2B;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
+
import { runWithCacheTenant } from "@open-mercato/cache";
|
|
2
3
|
import { actionEditSchema, validateActionPayloadForType } from "../../../../../data/validators.js";
|
|
3
4
|
import { emitInboxOpsEvent } from "../../../../../events.js";
|
|
5
|
+
import { resolveCache, invalidateCountsCache } from "../../../../../lib/cache.js";
|
|
4
6
|
import {
|
|
5
7
|
resolveRequestContext,
|
|
6
8
|
resolveActionAndProposal,
|
|
@@ -31,6 +33,8 @@ async function PATCH(req) {
|
|
|
31
33
|
}
|
|
32
34
|
action.payload = mergedPayload;
|
|
33
35
|
await ctx.em.flush();
|
|
36
|
+
const cache = resolveCache(ctx.container);
|
|
37
|
+
await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId));
|
|
34
38
|
try {
|
|
35
39
|
await emitInboxOpsEvent("inbox_ops.action.edited", {
|
|
36
40
|
actionId: action.id,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/inbox_ops/api/proposals/%5Bid%5D/actions/%5BactionId%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { actionEditSchema, validateActionPayloadForType } from '../../../../../data/validators'\nimport { emitInboxOpsEvent } from '../../../../../events'\nimport {\n resolveRequestContext,\n resolveActionAndProposal,\n handleRouteError,\n isErrorResponse,\n} from '../../../../routeHelpers'\n\nexport const metadata = {\n PATCH: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function PATCH(req: Request) {\n try {\n const body = await req.json()\n const parsed = actionEditSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.issues }, { status: 400 })\n }\n\n const ctx = await resolveRequestContext(req)\n const resolved = await resolveActionAndProposal(new URL(req.url), ctx)\n if (isErrorResponse(resolved)) return resolved\n\n const { action } = resolved\n\n if (action.status !== 'pending' && action.status !== 'failed') {\n return NextResponse.json({ error: 'Action already processed' }, { status: 409 })\n }\n\n const mergedPayload = { ...action.payload as Record<string, unknown>, ...parsed.data.payload }\n const payloadValidation = validateActionPayloadForType(action.actionType, mergedPayload)\n if (!payloadValidation.success) {\n return NextResponse.json({ error: payloadValidation.error }, { status: 400 })\n }\n\n action.payload = mergedPayload\n await ctx.em.flush()\n\n try {\n await emitInboxOpsEvent('inbox_ops.action.edited', {\n actionId: action.id,\n proposalId: action.proposalId,\n actionType: action.actionType,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n } catch (eventError) {\n console.error('[inbox_ops:action:edit] Failed to emit event:', eventError)\n }\n\n return NextResponse.json({ ok: true, action })\n } catch (err) {\n return handleRouteError(err, 'edit action')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Edit action',\n methods: {\n PATCH: {\n summary: 'Edit action payload before accepting',\n responses: [\n { status: 200, description: 'Action updated' },\n { status: 404, description: 'Action not found' },\n { status: 409, description: 'Action already processed' },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,kBAAkB,oCAAoC;AAC/D,SAAS,yBAAyB;AAClC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,OAAO,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC9E;AAEA,eAAsB,MAAM,KAAc;AACxC,MAAI;AACF,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAM,SAAS,iBAAiB,UAAU,IAAI;AAC9C,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO,aAAa,KAAK,EAAE,OAAO,mBAAmB,SAAS,OAAO,MAAM,OAAO,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACtG;AAEA,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAC3C,UAAM,WAAW,MAAM,yBAAyB,IAAI,IAAI,IAAI,GAAG,GAAG,GAAG;AACrE,QAAI,gBAAgB,QAAQ,EAAG,QAAO;AAEtC,UAAM,EAAE,OAAO,IAAI;AAEnB,QAAI,OAAO,WAAW,aAAa,OAAO,WAAW,UAAU;AAC7D,aAAO,aAAa,KAAK,EAAE,OAAO,2BAA2B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACjF;AAEA,UAAM,gBAAgB,EAAE,GAAG,OAAO,SAAoC,GAAG,OAAO,KAAK,QAAQ;AAC7F,UAAM,oBAAoB,6BAA6B,OAAO,YAAY,aAAa;AACvF,QAAI,CAAC,kBAAkB,SAAS;AAC9B,aAAO,aAAa,KAAK,EAAE,OAAO,kBAAkB,MAAM,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC9E;AAEA,WAAO,UAAU;AACjB,UAAM,IAAI,GAAG,MAAM;AAEnB,QAAI;AACF,YAAM,kBAAkB,2BAA2B;AAAA,QACjD,UAAU,OAAO;AAAA,QACjB,YAAY,OAAO;AAAA,QACnB,YAAY,OAAO;AAAA,QACnB,UAAU,IAAI;AAAA,QACd,gBAAgB,IAAI;AAAA,MACtB,CAAC;AAAA,IACH,SAAS,YAAY;AACnB,cAAQ,MAAM,iDAAiD,UAAU;AAAA,IAC3E;AAEA,WAAO,aAAa,KAAK,EAAE,IAAI,MAAM,OAAO,CAAC;AAAA,EAC/C,SAAS,KAAK;AACZ,WAAO,iBAAiB,KAAK,aAAa;AAAA,EAC5C;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,OAAO;AAAA,MACL,SAAS;AAAA,MACT,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,iBAAiB;AAAA,QAC7C,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,QAC/C,EAAE,QAAQ,KAAK,aAAa,2BAA2B;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AACF;",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { actionEditSchema, validateActionPayloadForType } from '../../../../../data/validators'\nimport { emitInboxOpsEvent } from '../../../../../events'\nimport { resolveCache, invalidateCountsCache } from '../../../../../lib/cache'\nimport {\n resolveRequestContext,\n resolveActionAndProposal,\n handleRouteError,\n isErrorResponse,\n} from '../../../../routeHelpers'\n\nexport const metadata = {\n PATCH: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function PATCH(req: Request) {\n try {\n const body = await req.json()\n const parsed = actionEditSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.issues }, { status: 400 })\n }\n\n const ctx = await resolveRequestContext(req)\n const resolved = await resolveActionAndProposal(new URL(req.url), ctx)\n if (isErrorResponse(resolved)) return resolved\n\n const { action } = resolved\n\n if (action.status !== 'pending' && action.status !== 'failed') {\n return NextResponse.json({ error: 'Action already processed' }, { status: 409 })\n }\n\n const mergedPayload = { ...action.payload as Record<string, unknown>, ...parsed.data.payload }\n const payloadValidation = validateActionPayloadForType(action.actionType, mergedPayload)\n if (!payloadValidation.success) {\n return NextResponse.json({ error: payloadValidation.error }, { status: 400 })\n }\n\n action.payload = mergedPayload\n await ctx.em.flush()\n\n const cache = resolveCache(ctx.container)\n await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))\n\n try {\n await emitInboxOpsEvent('inbox_ops.action.edited', {\n actionId: action.id,\n proposalId: action.proposalId,\n actionType: action.actionType,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n } catch (eventError) {\n console.error('[inbox_ops:action:edit] Failed to emit event:', eventError)\n }\n\n return NextResponse.json({ ok: true, action })\n } catch (err) {\n return handleRouteError(err, 'edit action')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Edit action',\n methods: {\n PATCH: {\n summary: 'Edit action payload before accepting',\n responses: [\n { status: 200, description: 'Action updated' },\n { status: 404, description: 'Action not found' },\n { status: 409, description: 'Action already processed' },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AACnC,SAAS,kBAAkB,oCAAoC;AAC/D,SAAS,yBAAyB;AAClC,SAAS,cAAc,6BAA6B;AACpD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,OAAO,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC9E;AAEA,eAAsB,MAAM,KAAc;AACxC,MAAI;AACF,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAM,SAAS,iBAAiB,UAAU,IAAI;AAC9C,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO,aAAa,KAAK,EAAE,OAAO,mBAAmB,SAAS,OAAO,MAAM,OAAO,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACtG;AAEA,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAC3C,UAAM,WAAW,MAAM,yBAAyB,IAAI,IAAI,IAAI,GAAG,GAAG,GAAG;AACrE,QAAI,gBAAgB,QAAQ,EAAG,QAAO;AAEtC,UAAM,EAAE,OAAO,IAAI;AAEnB,QAAI,OAAO,WAAW,aAAa,OAAO,WAAW,UAAU;AAC7D,aAAO,aAAa,KAAK,EAAE,OAAO,2BAA2B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACjF;AAEA,UAAM,gBAAgB,EAAE,GAAG,OAAO,SAAoC,GAAG,OAAO,KAAK,QAAQ;AAC7F,UAAM,oBAAoB,6BAA6B,OAAO,YAAY,aAAa;AACvF,QAAI,CAAC,kBAAkB,SAAS;AAC9B,aAAO,aAAa,KAAK,EAAE,OAAO,kBAAkB,MAAM,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC9E;AAEA,WAAO,UAAU;AACjB,UAAM,IAAI,GAAG,MAAM;AAEnB,UAAM,QAAQ,aAAa,IAAI,SAAS;AACxC,UAAM,mBAAmB,IAAI,UAAU,MAAM,sBAAsB,OAAO,IAAI,QAAQ,CAAC;AAEvF,QAAI;AACF,YAAM,kBAAkB,2BAA2B;AAAA,QACjD,UAAU,OAAO;AAAA,QACjB,YAAY,OAAO;AAAA,QACnB,YAAY,OAAO;AAAA,QACnB,UAAU,IAAI;AAAA,QACd,gBAAgB,IAAI;AAAA,MACtB,CAAC;AAAA,IACH,SAAS,YAAY;AACnB,cAAQ,MAAM,iDAAiD,UAAU;AAAA,IAC3E;AAEA,WAAO,aAAa,KAAK,EAAE,IAAI,MAAM,OAAO,CAAC;AAAA,EAC/C,SAAS,KAAK;AACZ,WAAO,iBAAiB,KAAK,aAAa;AAAA,EAC5C;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,OAAO;AAAA,MACL,SAAS;AAAA,MACT,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,iBAAiB;AAAA,QAC7C,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,QAC/C,EAAE,QAAQ,KAAK,aAAa,2BAA2B;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { runWithCacheTenant } from "@open-mercato/cache";
|
|
3
|
+
import { categorizeProposalSchema } from "../../../../data/validators.js";
|
|
4
|
+
import { resolveCache, invalidateCountsCache } from "../../../../lib/cache.js";
|
|
5
|
+
import {
|
|
6
|
+
resolveRequestContext,
|
|
7
|
+
resolveProposal,
|
|
8
|
+
handleRouteError,
|
|
9
|
+
isErrorResponse
|
|
10
|
+
} from "../../../routeHelpers.js";
|
|
11
|
+
const metadata = {
|
|
12
|
+
POST: { requireAuth: true, requireFeatures: ["inbox_ops.proposals.manage"] }
|
|
13
|
+
};
|
|
14
|
+
async function POST(req) {
|
|
15
|
+
try {
|
|
16
|
+
const ctx = await resolveRequestContext(req);
|
|
17
|
+
const proposal = await resolveProposal(new URL(req.url), ctx);
|
|
18
|
+
if (isErrorResponse(proposal)) return proposal;
|
|
19
|
+
const body = await req.json();
|
|
20
|
+
const parsed = categorizeProposalSchema.safeParse(body);
|
|
21
|
+
if (!parsed.success) {
|
|
22
|
+
const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
23
|
+
return NextResponse.json({ error: `Invalid category: ${issues}` }, { status: 400 });
|
|
24
|
+
}
|
|
25
|
+
const previousCategory = proposal.category || null;
|
|
26
|
+
proposal.category = parsed.data.category;
|
|
27
|
+
await ctx.em.flush();
|
|
28
|
+
const cache = resolveCache(ctx.container);
|
|
29
|
+
await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId));
|
|
30
|
+
return NextResponse.json({
|
|
31
|
+
ok: true,
|
|
32
|
+
category: parsed.data.category,
|
|
33
|
+
previousCategory
|
|
34
|
+
});
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return handleRouteError(err, "categorize proposal");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const openApi = {
|
|
40
|
+
tag: "InboxOps",
|
|
41
|
+
summary: "Categorize proposal",
|
|
42
|
+
methods: {
|
|
43
|
+
POST: {
|
|
44
|
+
summary: "Set or change the category of a proposal",
|
|
45
|
+
description: "Assigns a category to a proposal. Returns the new and previous category for undo support.",
|
|
46
|
+
responses: [
|
|
47
|
+
{ status: 200, description: "Category updated" },
|
|
48
|
+
{ status: 400, description: "Invalid category value" },
|
|
49
|
+
{ status: 404, description: "Proposal not found" }
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
export {
|
|
55
|
+
POST,
|
|
56
|
+
metadata,
|
|
57
|
+
openApi
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../src/modules/inbox_ops/api/proposals/%5Bid%5D/categorize/route.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { categorizeProposalSchema } from '../../../../data/validators'\nimport { resolveCache, invalidateCountsCache } from '../../../../lib/cache'\nimport {\n resolveRequestContext,\n resolveProposal,\n handleRouteError,\n isErrorResponse,\n} from '../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const proposal = await resolveProposal(new URL(req.url), ctx)\n if (isErrorResponse(proposal)) return proposal\n\n const body = await req.json()\n const parsed = categorizeProposalSchema.safeParse(body)\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')\n return NextResponse.json({ error: `Invalid category: ${issues}` }, { status: 400 })\n }\n\n const previousCategory = proposal.category || null\n proposal.category = parsed.data.category\n await ctx.em.flush()\n\n const cache = resolveCache(ctx.container)\n await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))\n\n return NextResponse.json({\n ok: true,\n category: parsed.data.category,\n previousCategory,\n })\n } catch (err) {\n return handleRouteError(err, 'categorize proposal')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Categorize proposal',\n methods: {\n POST: {\n summary: 'Set or change the category of a proposal',\n description: 'Assigns a category to a proposal. Returns the new and previous category for undo support.',\n responses: [\n { status: 200, description: 'Category updated' },\n { status: 400, description: 'Invalid category value' },\n { status: 404, description: 'Proposal not found' },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AACnC,SAAS,gCAAgC;AACzC,SAAS,cAAc,6BAA6B;AACpD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAC3C,UAAM,WAAW,MAAM,gBAAgB,IAAI,IAAI,IAAI,GAAG,GAAG,GAAG;AAC5D,QAAI,gBAAgB,QAAQ,EAAG,QAAO;AAEtC,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAM,SAAS,yBAAyB,UAAU,IAAI;AACtD,QAAI,CAAC,OAAO,SAAS;AACnB,YAAM,SAAS,OAAO,MAAM,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,KAAK,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI;AAC5F,aAAO,aAAa,KAAK,EAAE,OAAO,qBAAqB,MAAM,GAAG,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpF;AAEA,UAAM,mBAAmB,SAAS,YAAY;AAC9C,aAAS,WAAW,OAAO,KAAK;AAChC,UAAM,IAAI,GAAG,MAAM;AAEnB,UAAM,QAAQ,aAAa,IAAI,SAAS;AACxC,UAAM,mBAAmB,IAAI,UAAU,MAAM,sBAAsB,OAAO,IAAI,QAAQ,CAAC;AAEvF,WAAO,aAAa,KAAK;AAAA,MACvB,IAAI;AAAA,MACJ,UAAU,OAAO,KAAK;AAAA,MACtB;AAAA,IACF,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,WAAO,iBAAiB,KAAK,qBAAqB;AAAA,EACpD;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,QAC/C,EAAE,QAAQ,KAAK,aAAa,yBAAyB;AAAA,QACrD,EAAE,QAAQ,KAAK,aAAa,qBAAqB;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
+
import { runWithCacheTenant } from "@open-mercato/cache";
|
|
2
3
|
import { rejectProposal } from "../../../../lib/executionEngine.js";
|
|
4
|
+
import { resolveCache, invalidateCountsCache } from "../../../../lib/cache.js";
|
|
3
5
|
import {
|
|
4
6
|
resolveRequestContext,
|
|
5
7
|
resolveProposal,
|
|
@@ -16,6 +18,8 @@ async function POST(req) {
|
|
|
16
18
|
const proposal = await resolveProposal(new URL(req.url), ctx);
|
|
17
19
|
if (isErrorResponse(proposal)) return proposal;
|
|
18
20
|
await rejectProposal(proposal.id, toExecutionContext(ctx));
|
|
21
|
+
const cache = resolveCache(ctx.container);
|
|
22
|
+
await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId));
|
|
19
23
|
return NextResponse.json({ ok: true });
|
|
20
24
|
} catch (err) {
|
|
21
25
|
return handleRouteError(err, "reject proposal");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/inbox_ops/api/proposals/%5Bid%5D/reject/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { rejectProposal } from '../../../../lib/executionEngine'\nimport {\n resolveRequestContext,\n resolveProposal,\n toExecutionContext,\n handleRouteError,\n isErrorResponse,\n} from '../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const proposal = await resolveProposal(new URL(req.url), ctx)\n if (isErrorResponse(proposal)) return proposal\n\n await rejectProposal(proposal.id, toExecutionContext(ctx))\n\n return NextResponse.json({ ok: true })\n } catch (err) {\n return handleRouteError(err, 'reject proposal')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Reject proposal',\n methods: {\n POST: {\n summary: 'Reject entire proposal (all pending actions)',\n responses: [\n { status: 200, description: 'Proposal rejected' },\n { status: 404, description: 'Proposal not found' },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,sBAAsB;AAC/B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAC3C,UAAM,WAAW,MAAM,gBAAgB,IAAI,IAAI,IAAI,GAAG,GAAG,GAAG;AAC5D,QAAI,gBAAgB,QAAQ,EAAG,QAAO;AAEtC,UAAM,eAAe,SAAS,IAAI,mBAAmB,GAAG,CAAC;AAEzD,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC,SAAS,KAAK;AACZ,WAAO,iBAAiB,KAAK,iBAAiB;AAAA,EAChD;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,oBAAoB;AAAA,QAChD,EAAE,QAAQ,KAAK,aAAa,qBAAqB;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AACF;",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { rejectProposal } from '../../../../lib/executionEngine'\nimport { resolveCache, invalidateCountsCache } from '../../../../lib/cache'\nimport {\n resolveRequestContext,\n resolveProposal,\n toExecutionContext,\n handleRouteError,\n isErrorResponse,\n} from '../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.manage'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const proposal = await resolveProposal(new URL(req.url), ctx)\n if (isErrorResponse(proposal)) return proposal\n\n await rejectProposal(proposal.id, toExecutionContext(ctx))\n\n const cache = resolveCache(ctx.container)\n await runWithCacheTenant(ctx.tenantId, () => invalidateCountsCache(cache, ctx.tenantId))\n\n return NextResponse.json({ ok: true })\n } catch (err) {\n return handleRouteError(err, 'reject proposal')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Reject proposal',\n methods: {\n POST: {\n summary: 'Reject entire proposal (all pending actions)',\n responses: [\n { status: 200, description: 'Proposal rejected' },\n { status: 404, description: 'Proposal not found' },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AACnC,SAAS,sBAAsB;AAC/B,SAAS,cAAc,6BAA6B;AACpD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAC3C,UAAM,WAAW,MAAM,gBAAgB,IAAI,IAAI,IAAI,GAAG,GAAG,GAAG;AAC5D,QAAI,gBAAgB,QAAQ,EAAG,QAAO;AAEtC,UAAM,eAAe,SAAS,IAAI,mBAAmB,GAAG,CAAC;AAEzD,UAAM,QAAQ,aAAa,IAAI,SAAS;AACxC,UAAM,mBAAmB,IAAI,UAAU,MAAM,sBAAsB,OAAO,IAAI,QAAQ,CAAC;AAEvF,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC,SAAS,KAAK;AACZ,WAAO,iBAAiB,KAAK,iBAAiB;AAAA,EAChD;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,oBAAoB;AAAA,QAChD,EAAE,QAAQ,KAAK,aAAa,qBAAqB;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,6 +5,7 @@ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find"
|
|
|
5
5
|
import { InboxProposalAction, InboxEmail } from "../../../../../../data/entities.js";
|
|
6
6
|
import { draftReplyPayloadSchema } from "../../../../../../data/validators.js";
|
|
7
7
|
import { emitInboxOpsEvent } from "../../../../../../events.js";
|
|
8
|
+
import { createMessageRecordForReply } from "../../../../../../lib/messagesIntegration.js";
|
|
8
9
|
import {
|
|
9
10
|
resolveRequestContext,
|
|
10
11
|
resolveProposal,
|
|
@@ -17,14 +18,6 @@ const metadata = {
|
|
|
17
18
|
};
|
|
18
19
|
async function POST(req) {
|
|
19
20
|
try {
|
|
20
|
-
const apiKey = process.env.RESEND_API_KEY;
|
|
21
|
-
if (!apiKey) {
|
|
22
|
-
return NextResponse.json({ error: "Email service not configured" }, { status: 503 });
|
|
23
|
-
}
|
|
24
|
-
const emailDisabled = parseBooleanWithDefault(process.env.OM_DISABLE_EMAIL_DELIVERY, false) || parseBooleanWithDefault(process.env.OM_TEST_MODE, false);
|
|
25
|
-
if (emailDisabled) {
|
|
26
|
-
return NextResponse.json({ error: "Email delivery is disabled" }, { status: 503 });
|
|
27
|
-
}
|
|
28
21
|
const ctx = await resolveRequestContext(req);
|
|
29
22
|
const url = new URL(req.url);
|
|
30
23
|
const proposal = await resolveProposal(url, ctx);
|
|
@@ -67,7 +60,16 @@ async function POST(req) {
|
|
|
67
60
|
if (!payloadResult.success) {
|
|
68
61
|
return NextResponse.json({ error: "Reply payload missing required fields (to, subject, body)" }, { status: 400 });
|
|
69
62
|
}
|
|
70
|
-
const { to: toAddress, subject, body
|
|
63
|
+
const { to: toAddress, toName, subject, body } = payloadResult.data;
|
|
64
|
+
const apiKey = process.env.RESEND_API_KEY;
|
|
65
|
+
if (!apiKey) {
|
|
66
|
+
return NextResponse.json({ error: "Email service not configured" }, { status: 503 });
|
|
67
|
+
}
|
|
68
|
+
const emailDisabled = parseBooleanWithDefault(process.env.OM_DISABLE_EMAIL_DELIVERY, false) || parseBooleanWithDefault(process.env.OM_TEST_MODE, false);
|
|
69
|
+
if (emailDisabled) {
|
|
70
|
+
return NextResponse.json({ error: "Email delivery is disabled" }, { status: 503 });
|
|
71
|
+
}
|
|
72
|
+
const { inReplyToMessageId, references: payloadReferences } = payloadResult.data;
|
|
71
73
|
const fromAddress = process.env.EMAIL_FROM || `inbox@${process.env.INBOX_OPS_DOMAIN || "inbox.mercato.local"}`;
|
|
72
74
|
const headers = {};
|
|
73
75
|
const inReplyTo = inReplyToMessageId || email?.messageId;
|
|
@@ -91,10 +93,23 @@ async function POST(req) {
|
|
|
91
93
|
return NextResponse.json({ error: `Failed to send email: ${errorMessage}` }, { status: 502 });
|
|
92
94
|
}
|
|
93
95
|
const sentMessageId = sendData?.id || null;
|
|
96
|
+
const messagesResult = await createMessageRecordForReply(
|
|
97
|
+
{ to: toAddress, toName, subject, body },
|
|
98
|
+
proposal.inboxEmailId,
|
|
99
|
+
{
|
|
100
|
+
container: ctx.container,
|
|
101
|
+
scope: {
|
|
102
|
+
tenantId: ctx.tenantId,
|
|
103
|
+
organizationId: ctx.organizationId,
|
|
104
|
+
userId: ctx.userId
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
);
|
|
94
108
|
action.metadata = {
|
|
95
109
|
...action.metadata && typeof action.metadata === "object" ? action.metadata : {},
|
|
96
110
|
replySentAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
97
|
-
sentMessageId
|
|
111
|
+
sentMessageId,
|
|
112
|
+
...messagesResult ? { messageRecordId: messagesResult.messageId } : {}
|
|
98
113
|
};
|
|
99
114
|
await ctx.em.flush();
|
|
100
115
|
try {
|
|
@@ -104,12 +119,17 @@ async function POST(req) {
|
|
|
104
119
|
tenantId: ctx.tenantId,
|
|
105
120
|
organizationId: ctx.organizationId,
|
|
106
121
|
toAddress,
|
|
107
|
-
sentMessageId
|
|
122
|
+
sentMessageId,
|
|
123
|
+
messageRecordId: messagesResult?.messageId ?? null
|
|
108
124
|
});
|
|
109
125
|
} catch (eventError) {
|
|
110
126
|
console.error("[inbox_ops:reply:send] Failed to emit event:", eventError);
|
|
111
127
|
}
|
|
112
|
-
return NextResponse.json({
|
|
128
|
+
return NextResponse.json({
|
|
129
|
+
ok: true,
|
|
130
|
+
sentMessageId,
|
|
131
|
+
...messagesResult ? { messageRecordId: messagesResult.messageId } : {}
|
|
132
|
+
});
|
|
113
133
|
} catch (err) {
|
|
114
134
|
return handleRouteError(err, "send reply");
|
|
115
135
|
}
|
|
@@ -119,8 +139,8 @@ const openApi = {
|
|
|
119
139
|
summary: "Send draft reply",
|
|
120
140
|
methods: {
|
|
121
141
|
POST: {
|
|
122
|
-
summary: "Send a draft reply email
|
|
123
|
-
description: "Sends the draft_reply action payload as an
|
|
142
|
+
summary: "Send a draft reply email and register it in messages when available",
|
|
143
|
+
description: "Sends the draft_reply action payload via the configured email provider. When the messages module is available, also records the sent reply as an internal message record. Sets In-Reply-To and References headers for threading.",
|
|
124
144
|
responses: [
|
|
125
145
|
{ status: 200, description: "Reply sent successfully" },
|
|
126
146
|
{ status: 400, description: "Missing required payload fields" },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../../src/modules/inbox_ops/api/proposals/%5Bid%5D/replies/%5BreplyId%5D/send/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { Resend } from 'resend'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { InboxProposalAction, InboxEmail } from '../../../../../../data/entities'\nimport { draftReplyPayloadSchema } from '../../../../../../data/validators'\nimport { emitInboxOpsEvent } from '../../../../../../events'\nimport {\n resolveRequestContext,\n resolveProposal,\n extractPathSegment,\n handleRouteError,\n isErrorResponse,\n} from '../../../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.replies.send'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,cAAc;AACvB,SAAS,+BAA+B;AAExC,SAAS,6BAA6B;AACtC,SAAS,qBAAqB,kBAAkB;AAChD,SAAS,+BAA+B;AACxC,SAAS,yBAAyB;AAClC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,wBAAwB,EAAE;AACzE;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { Resend } from 'resend'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { InboxProposalAction, InboxEmail } from '../../../../../../data/entities'\nimport { draftReplyPayloadSchema } from '../../../../../../data/validators'\nimport { emitInboxOpsEvent } from '../../../../../../events'\nimport { createMessageRecordForReply } from '../../../../../../lib/messagesIntegration'\nimport {\n resolveRequestContext,\n resolveProposal,\n extractPathSegment,\n handleRouteError,\n isErrorResponse,\n} from '../../../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.replies.send'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const url = new URL(req.url)\n const proposal = await resolveProposal(url, ctx)\n if (isErrorResponse(proposal)) return proposal\n\n const replyId = extractPathSegment(url, 'replies')\n if (!replyId) {\n return NextResponse.json({ error: 'Missing reply ID' }, { status: 400 })\n }\n\n const action = await findOneWithDecryption(\n ctx.em,\n InboxProposalAction,\n {\n id: replyId,\n proposalId: proposal.id,\n actionType: 'draft_reply',\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n deletedAt: null,\n },\n undefined,\n ctx.scope,\n )\n\n if (!action) {\n return NextResponse.json({ error: 'Reply action not found' }, { status: 404 })\n }\n\n if (action.status !== 'executed') {\n return NextResponse.json(\n { error: `Cannot send reply \u2014 action must be accepted first (current status: \"${action.status}\")` },\n { status: 409 },\n )\n }\n\n const email = await findOneWithDecryption(\n ctx.em,\n InboxEmail,\n { id: proposal.inboxEmailId, deletedAt: null },\n undefined,\n ctx.scope,\n )\n\n const payloadResult = draftReplyPayloadSchema.safeParse(action.payload)\n if (!payloadResult.success) {\n return NextResponse.json({ error: 'Reply payload missing required fields (to, subject, body)' }, { status: 400 })\n }\n const { to: toAddress, toName, subject, body } = payloadResult.data\n\n const apiKey = process.env.RESEND_API_KEY\n if (!apiKey) {\n return NextResponse.json({ error: 'Email service not configured' }, { status: 503 })\n }\n\n const emailDisabled =\n parseBooleanWithDefault(process.env.OM_DISABLE_EMAIL_DELIVERY, false) ||\n parseBooleanWithDefault(process.env.OM_TEST_MODE, false)\n if (emailDisabled) {\n return NextResponse.json({ error: 'Email delivery is disabled' }, { status: 503 })\n }\n\n const { inReplyToMessageId, references: payloadReferences } = payloadResult.data\n const fromAddress = process.env.EMAIL_FROM || `inbox@${process.env.INBOX_OPS_DOMAIN || 'inbox.mercato.local'}`\n\n const headers: Record<string, string> = {}\n const inReplyTo = inReplyToMessageId || email?.messageId\n if (inReplyTo) {\n headers['In-Reply-To'] = inReplyTo\n }\n const references = payloadReferences || (email?.emailReferences as string[])\n if (references && references.length > 0) {\n headers['References'] = references.join(' ')\n }\n\n const resend = new Resend(apiKey)\n const { data: sendData, error: sendError } = await resend.emails.send({\n to: toAddress,\n from: fromAddress,\n subject,\n text: body,\n ...(Object.keys(headers).length > 0 ? { headers } : {}),\n })\n\n if (sendError) {\n const errorMessage = sendError.message || 'Unknown error'\n return NextResponse.json({ error: `Failed to send email: ${errorMessage}` }, { status: 502 })\n }\n\n const sentMessageId = sendData?.id || null\n const messagesResult = await createMessageRecordForReply(\n { to: toAddress, toName, subject, body },\n proposal.inboxEmailId,\n {\n container: ctx.container,\n scope: {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n },\n },\n )\n\n action.metadata = {\n ...(action.metadata && typeof action.metadata === 'object' ? action.metadata : {}),\n replySentAt: new Date().toISOString(),\n sentMessageId,\n ...(messagesResult ? { messageRecordId: messagesResult.messageId } : {}),\n }\n await ctx.em.flush()\n\n try {\n await emitInboxOpsEvent('inbox_ops.reply.sent', {\n proposalId: proposal.id,\n actionId: replyId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n toAddress,\n sentMessageId,\n messageRecordId: messagesResult?.messageId ?? null,\n })\n } catch (eventError) {\n console.error('[inbox_ops:reply:send] Failed to emit event:', eventError)\n }\n\n return NextResponse.json({\n ok: true,\n sentMessageId,\n ...(messagesResult ? { messageRecordId: messagesResult.messageId } : {}),\n })\n } catch (err) {\n return handleRouteError(err, 'send reply')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Send draft reply',\n methods: {\n POST: {\n summary: 'Send a draft reply email and register it in messages when available',\n description: 'Sends the draft_reply action payload via the configured email provider. When the messages module is available, also records the sent reply as an internal message record. Sets In-Reply-To and References headers for threading.',\n responses: [\n { status: 200, description: 'Reply sent successfully' },\n { status: 400, description: 'Missing required payload fields' },\n { status: 404, description: 'Reply action not found' },\n { status: 409, description: 'Action in invalid state for sending' },\n { status: 502, description: 'Email delivery failed' },\n { status: 503, description: 'Email service not configured or disabled' },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
+
import { runWithCacheTenant } from "@open-mercato/cache";
|
|
2
3
|
import { InboxProposal } from "../../../data/entities.js";
|
|
4
|
+
import { ALL_CATEGORIES } from "../../../data/validators.js";
|
|
3
5
|
import { resolveRequestContext, UnauthorizedError } from "../../routeHelpers.js";
|
|
6
|
+
import {
|
|
7
|
+
resolveCache,
|
|
8
|
+
createCountsCacheKey,
|
|
9
|
+
createCountsCacheTag,
|
|
10
|
+
COUNTS_CACHE_TTL_MS
|
|
11
|
+
} from "../../../lib/cache.js";
|
|
4
12
|
const metadata = {
|
|
5
13
|
GET: { requireAuth: true, requireFeatures: ["inbox_ops.proposals.view"] }
|
|
6
14
|
};
|
|
7
15
|
async function GET(req) {
|
|
8
16
|
try {
|
|
9
17
|
const ctx = await resolveRequestContext(req);
|
|
18
|
+
const cache = resolveCache(ctx.container);
|
|
19
|
+
if (cache) {
|
|
20
|
+
const cacheKey = createCountsCacheKey(ctx.tenantId);
|
|
21
|
+
const cached = await runWithCacheTenant(ctx.tenantId, () => cache.get(cacheKey));
|
|
22
|
+
if (cached) {
|
|
23
|
+
return NextResponse.json(cached);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
10
26
|
const scope = {
|
|
11
27
|
organizationId: ctx.organizationId,
|
|
12
28
|
tenantId: ctx.tenantId,
|
|
@@ -19,7 +35,36 @@ async function GET(req) {
|
|
|
19
35
|
ctx.em.count(InboxProposal, { ...scope, status: "accepted" }),
|
|
20
36
|
ctx.em.count(InboxProposal, { ...scope, status: "rejected" })
|
|
21
37
|
]);
|
|
22
|
-
|
|
38
|
+
const knex = ctx.em.getKnex();
|
|
39
|
+
const categoryRows = await knex("inbox_proposals").select("category").count("* as count").where({
|
|
40
|
+
organization_id: ctx.organizationId,
|
|
41
|
+
tenant_id: ctx.tenantId,
|
|
42
|
+
is_active: true
|
|
43
|
+
}).whereNull("deleted_at").groupBy("category");
|
|
44
|
+
const byCategory = {};
|
|
45
|
+
for (const cat of ALL_CATEGORIES) {
|
|
46
|
+
byCategory[cat] = 0;
|
|
47
|
+
}
|
|
48
|
+
for (const row of categoryRows) {
|
|
49
|
+
const cat = row.category;
|
|
50
|
+
if (cat && cat in byCategory) {
|
|
51
|
+
byCategory[cat] = Number(row.count);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const responseBody = { pending, partial, accepted, rejected, byCategory };
|
|
55
|
+
if (cache) {
|
|
56
|
+
const cacheKey = createCountsCacheKey(ctx.tenantId);
|
|
57
|
+
const tag = createCountsCacheTag(ctx.tenantId);
|
|
58
|
+
try {
|
|
59
|
+
await runWithCacheTenant(
|
|
60
|
+
ctx.tenantId,
|
|
61
|
+
() => cache.set(cacheKey, responseBody, { ttl: COUNTS_CACHE_TTL_MS, tags: [tag] })
|
|
62
|
+
);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.warn("[inbox_ops:proposals:counts] Failed to set cache", err);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return NextResponse.json(responseBody);
|
|
23
68
|
} catch (err) {
|
|
24
69
|
if (err instanceof UnauthorizedError) {
|
|
25
70
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
@@ -33,10 +78,10 @@ const openApi = {
|
|
|
33
78
|
summary: "Proposal counts",
|
|
34
79
|
methods: {
|
|
35
80
|
GET: {
|
|
36
|
-
summary: "Get proposal status counts",
|
|
37
|
-
description: "Returns counts by status for tab badges",
|
|
81
|
+
summary: "Get proposal status and category counts",
|
|
82
|
+
description: "Returns counts by status and by category for tab badges and filter dropdowns",
|
|
38
83
|
responses: [
|
|
39
|
-
{ status: 200, description: "Status counts object" }
|
|
84
|
+
{ status: 200, description: "Status and category counts object" }
|
|
40
85
|
]
|
|
41
86
|
}
|
|
42
87
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/inbox_ops/api/proposals/counts/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { InboxProposal } from '../../../data/entities'\nimport { resolveRequestContext, UnauthorizedError } from '../../routeHelpers'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.view'] },\n}\n\nexport async function GET(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n\n const scope = {\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n deletedAt: null,\n isActive: true,\n }\n\n // em.count() is safe here \u2014 filter fields (status, organizationId, tenantId,\n // deletedAt, isActive) are not encrypted, so decryption helpers are not needed.\n const [pending, partial, accepted, rejected] = await Promise.all([\n ctx.em.count(InboxProposal, { ...scope, status: 'pending' }),\n ctx.em.count(InboxProposal, { ...scope, status: 'partial' }),\n ctx.em.count(InboxProposal, { ...scope, status: 'accepted' }),\n ctx.em.count(InboxProposal, { ...scope, status: 'rejected' }),\n ])\n\n
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB,yBAAyB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { InboxProposal } from '../../../data/entities'\nimport { ALL_CATEGORIES } from '../../../data/validators'\nimport { resolveRequestContext, UnauthorizedError } from '../../routeHelpers'\nimport {\n resolveCache,\n createCountsCacheKey,\n createCountsCacheTag,\n COUNTS_CACHE_TTL_MS,\n} from '../../../lib/cache'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.view'] },\n}\n\nexport async function GET(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const cache = resolveCache(ctx.container)\n\n if (cache) {\n const cacheKey = createCountsCacheKey(ctx.tenantId)\n const cached = await runWithCacheTenant(ctx.tenantId, () => cache.get(cacheKey))\n if (cached) {\n return NextResponse.json(cached)\n }\n }\n\n const scope = {\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n deletedAt: null,\n isActive: true,\n }\n\n // em.count() is safe here \u2014 filter fields (status, organizationId, tenantId,\n // deletedAt, isActive, category) are not encrypted, so decryption helpers are not needed.\n const [pending, partial, accepted, rejected] = await Promise.all([\n ctx.em.count(InboxProposal, { ...scope, status: 'pending' }),\n ctx.em.count(InboxProposal, { ...scope, status: 'partial' }),\n ctx.em.count(InboxProposal, { ...scope, status: 'accepted' }),\n ctx.em.count(InboxProposal, { ...scope, status: 'rejected' }),\n ])\n\n // Single GROUP BY query for category counts \u2014 O(1) queries\n const knex = ctx.em.getKnex()\n const categoryRows = await knex('inbox_proposals')\n .select('category')\n .count('* as count')\n .where({\n organization_id: ctx.organizationId,\n tenant_id: ctx.tenantId,\n is_active: true,\n })\n .whereNull('deleted_at')\n .groupBy('category')\n\n const byCategory: Record<string, number> = {}\n for (const cat of ALL_CATEGORIES) {\n byCategory[cat] = 0\n }\n for (const row of categoryRows) {\n const cat = row.category as string | null\n if (cat && cat in byCategory) {\n byCategory[cat] = Number(row.count)\n }\n }\n\n const responseBody = { pending, partial, accepted, rejected, byCategory }\n\n if (cache) {\n const cacheKey = createCountsCacheKey(ctx.tenantId)\n const tag = createCountsCacheTag(ctx.tenantId)\n try {\n await runWithCacheTenant(ctx.tenantId, () =>\n cache.set(cacheKey, responseBody, { ttl: COUNTS_CACHE_TTL_MS, tags: [tag] }),\n )\n } catch (err) {\n console.warn('[inbox_ops:proposals:counts] Failed to set cache', err)\n }\n }\n\n return NextResponse.json(responseBody)\n } catch (err) {\n if (err instanceof UnauthorizedError) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n console.error('[inbox_ops:proposals:counts] Error:', err)\n return NextResponse.json({ error: 'Failed to get counts' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Proposal counts',\n methods: {\n GET: {\n summary: 'Get proposal status and category counts',\n description: 'Returns counts by status and by category for tab badges and filter dropdowns',\n responses: [\n { status: 200, description: 'Status and category counts object' },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
+
import { ZodError } from "zod";
|
|
2
3
|
import { findAndCountWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
4
|
import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
|
|
4
5
|
import { InboxProposal, InboxEmail, InboxProposalAction, InboxDiscrepancy } from "../../data/entities.js";
|
|
@@ -12,6 +13,7 @@ async function GET(req) {
|
|
|
12
13
|
const url = new URL(req.url);
|
|
13
14
|
const query = proposalListQuerySchema.parse({
|
|
14
15
|
status: url.searchParams.get("status") || void 0,
|
|
16
|
+
category: url.searchParams.get("category") || void 0,
|
|
15
17
|
search: url.searchParams.get("search") || void 0,
|
|
16
18
|
page: url.searchParams.get("page") || void 0,
|
|
17
19
|
pageSize: url.searchParams.get("pageSize") || void 0
|
|
@@ -26,6 +28,14 @@ async function GET(req) {
|
|
|
26
28
|
if (query.status) {
|
|
27
29
|
where.status = query.status;
|
|
28
30
|
}
|
|
31
|
+
if (query.category) {
|
|
32
|
+
const categories = query.category.split(",").map((c) => c.trim()).filter(Boolean);
|
|
33
|
+
if (categories.length === 1) {
|
|
34
|
+
where.category = categories[0];
|
|
35
|
+
} else if (categories.length > 1) {
|
|
36
|
+
where.category = { $in: categories };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
29
39
|
if (query.search) {
|
|
30
40
|
where.summary = { $ilike: `%${escapeLikePattern(query.search)}%` };
|
|
31
41
|
}
|
|
@@ -71,6 +81,9 @@ async function GET(req) {
|
|
|
71
81
|
totalPages: Math.ceil(total / query.pageSize)
|
|
72
82
|
});
|
|
73
83
|
} catch (err) {
|
|
84
|
+
if (err instanceof ZodError) {
|
|
85
|
+
return NextResponse.json({ error: "Invalid query parameters" }, { status: 400 });
|
|
86
|
+
}
|
|
74
87
|
if (err instanceof UnauthorizedError) {
|
|
75
88
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
76
89
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/inbox_ops/api/proposals/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { FilterQuery } from '@mikro-orm/postgresql'\nimport { findAndCountWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { InboxProposal, InboxEmail, InboxProposalAction, InboxDiscrepancy } from '../../data/entities'\nimport { proposalListQuerySchema } from '../../data/validators'\nimport { resolveRequestContext, UnauthorizedError } from '../routeHelpers'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.view'] },\n}\n\nexport async function GET(req: Request) {\n try {\n const url = new URL(req.url)\n const query = proposalListQuerySchema.parse({\n status: url.searchParams.get('status') || undefined,\n search: url.searchParams.get('search') || undefined,\n page: url.searchParams.get('page') || undefined,\n pageSize: url.searchParams.get('pageSize') || undefined,\n })\n\n const ctx = await resolveRequestContext(req)\n\n const where: FilterQuery<InboxProposal> = {\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n deletedAt: null,\n isActive: true,\n }\n\n if (query.status) {\n where.status = query.status\n }\n if (query.search) {\n where.summary = { $ilike: `%${escapeLikePattern(query.search)}%` }\n }\n\n const offset = (query.page - 1) * query.pageSize\n\n const [items, total] = await findAndCountWithDecryption(\n ctx.em,\n InboxProposal,\n where,\n {\n limit: query.pageSize,\n offset,\n orderBy: { createdAt: 'DESC' },\n },\n ctx.scope,\n )\n\n // Enrich proposals with email, action, and discrepancy data\n const proposalIds = items.map((p) => p.id)\n const emailIds = [...new Set(items.map((p) => p.inboxEmailId).filter(Boolean))]\n\n const [emails, allActions, allDiscrepancies] = await Promise.all([\n emailIds.length > 0\n ? findWithDecryption(ctx.em, InboxEmail, { id: { $in: emailIds }, organizationId: ctx.organizationId, tenantId: ctx.tenantId }, {}, ctx.scope)\n : Promise.resolve([] as InboxEmail[]),\n proposalIds.length > 0\n ? findWithDecryption(ctx.em, InboxProposalAction, { proposalId: { $in: proposalIds }, organizationId: ctx.organizationId, tenantId: ctx.tenantId, deletedAt: null }, {}, ctx.scope)\n : Promise.resolve([] as InboxProposalAction[]),\n proposalIds.length > 0\n ? findWithDecryption(ctx.em, InboxDiscrepancy, { proposalId: { $in: proposalIds }, organizationId: ctx.organizationId, tenantId: ctx.tenantId, resolved: false }, {}, ctx.scope)\n : Promise.resolve([] as InboxDiscrepancy[]),\n ])\n\n const emailMap = new Map(emails.map((e) => [e.id, e]))\n\n const enrichedItems = items.map((proposal) => {\n const email = emailMap.get(proposal.inboxEmailId)\n const proposalActions = allActions.filter((a) => a.proposalId === proposal.id)\n const proposalDiscrepancies = allDiscrepancies.filter((d) => d.proposalId === proposal.id)\n\n return {\n ...proposal,\n actionCount: proposalActions.length,\n pendingActionCount: proposalActions.filter((a) => a.status === 'pending').length,\n discrepancyCount: proposalDiscrepancies.length,\n emailSubject: email?.subject || null,\n emailFrom: email?.forwardedByName || email?.forwardedByAddress || null,\n receivedAt: email?.receivedAt || proposal.createdAt,\n }\n })\n\n return NextResponse.json({\n items: enrichedItems,\n total,\n page: query.page,\n pageSize: query.pageSize,\n totalPages: Math.ceil(total / query.pageSize),\n })\n } catch (err) {\n if (err instanceof UnauthorizedError) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n console.error('[inbox_ops:proposals] Error listing proposals:', err)\n return NextResponse.json({ error: 'Failed to list proposals' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Proposals',\n methods: {\n GET: {\n summary: 'List proposals',\n description: 'List inbox proposals with optional status filter and pagination',\n responses: [\n { status: 200, description: 'Paginated list of proposals' },\n { status: 401, description: 'Unauthorized' },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { ZodError } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { FilterQuery } from '@mikro-orm/postgresql'\nimport { findAndCountWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { InboxProposal, InboxEmail, InboxProposalAction, InboxDiscrepancy, type InboxProposalCategory } from '../../data/entities'\nimport { proposalListQuerySchema } from '../../data/validators'\nimport { resolveRequestContext, UnauthorizedError } from '../routeHelpers'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['inbox_ops.proposals.view'] },\n}\n\nexport async function GET(req: Request) {\n try {\n const url = new URL(req.url)\n const query = proposalListQuerySchema.parse({\n status: url.searchParams.get('status') || undefined,\n category: url.searchParams.get('category') || undefined,\n search: url.searchParams.get('search') || undefined,\n page: url.searchParams.get('page') || undefined,\n pageSize: url.searchParams.get('pageSize') || undefined,\n })\n\n const ctx = await resolveRequestContext(req)\n\n const where: FilterQuery<InboxProposal> = {\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n deletedAt: null,\n isActive: true,\n }\n\n if (query.status) {\n where.status = query.status\n }\n if (query.category) {\n const categories = query.category.split(',').map((c) => c.trim()).filter(Boolean) as InboxProposalCategory[]\n if (categories.length === 1) {\n where.category = categories[0]\n } else if (categories.length > 1) {\n where.category = { $in: categories }\n }\n }\n if (query.search) {\n where.summary = { $ilike: `%${escapeLikePattern(query.search)}%` }\n }\n\n const offset = (query.page - 1) * query.pageSize\n\n const [items, total] = await findAndCountWithDecryption(\n ctx.em,\n InboxProposal,\n where,\n {\n limit: query.pageSize,\n offset,\n orderBy: { createdAt: 'DESC' },\n },\n ctx.scope,\n )\n\n // Enrich proposals with email, action, and discrepancy data\n const proposalIds = items.map((p) => p.id)\n const emailIds = [...new Set(items.map((p) => p.inboxEmailId).filter(Boolean))]\n\n const [emails, allActions, allDiscrepancies] = await Promise.all([\n emailIds.length > 0\n ? findWithDecryption(ctx.em, InboxEmail, { id: { $in: emailIds }, organizationId: ctx.organizationId, tenantId: ctx.tenantId }, {}, ctx.scope)\n : Promise.resolve([] as InboxEmail[]),\n proposalIds.length > 0\n ? findWithDecryption(ctx.em, InboxProposalAction, { proposalId: { $in: proposalIds }, organizationId: ctx.organizationId, tenantId: ctx.tenantId, deletedAt: null }, {}, ctx.scope)\n : Promise.resolve([] as InboxProposalAction[]),\n proposalIds.length > 0\n ? findWithDecryption(ctx.em, InboxDiscrepancy, { proposalId: { $in: proposalIds }, organizationId: ctx.organizationId, tenantId: ctx.tenantId, resolved: false }, {}, ctx.scope)\n : Promise.resolve([] as InboxDiscrepancy[]),\n ])\n\n const emailMap = new Map(emails.map((e) => [e.id, e]))\n\n const enrichedItems = items.map((proposal) => {\n const email = emailMap.get(proposal.inboxEmailId)\n const proposalActions = allActions.filter((a) => a.proposalId === proposal.id)\n const proposalDiscrepancies = allDiscrepancies.filter((d) => d.proposalId === proposal.id)\n\n return {\n ...proposal,\n actionCount: proposalActions.length,\n pendingActionCount: proposalActions.filter((a) => a.status === 'pending').length,\n discrepancyCount: proposalDiscrepancies.length,\n emailSubject: email?.subject || null,\n emailFrom: email?.forwardedByName || email?.forwardedByAddress || null,\n receivedAt: email?.receivedAt || proposal.createdAt,\n }\n })\n\n return NextResponse.json({\n items: enrichedItems,\n total,\n page: query.page,\n pageSize: query.pageSize,\n totalPages: Math.ceil(total / query.pageSize),\n })\n } catch (err) {\n if (err instanceof ZodError) {\n return NextResponse.json({ error: 'Invalid query parameters' }, { status: 400 })\n }\n if (err instanceof UnauthorizedError) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n console.error('[inbox_ops:proposals] Error listing proposals:', err)\n return NextResponse.json({ error: 'Failed to list proposals' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Proposals',\n methods: {\n GET: {\n summary: 'List proposals',\n description: 'List inbox proposals with optional status filter and pagination',\n responses: [\n { status: 200, description: 'Paginated list of proposals' },\n { status: 401, description: 'Unauthorized' },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
+
import { runWithCacheTenant } from "@open-mercato/cache";
|
|
2
3
|
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
4
|
import { InboxSettings } from "../../data/entities.js";
|
|
4
5
|
import { updateSettingsSchema } from "../../data/validators.js";
|
|
5
6
|
import { resolveRequestContext, handleRouteError } from "../routeHelpers.js";
|
|
7
|
+
import {
|
|
8
|
+
resolveCache,
|
|
9
|
+
createSettingsCacheKey,
|
|
10
|
+
createSettingsCacheTag,
|
|
11
|
+
invalidateSettingsCache,
|
|
12
|
+
SETTINGS_CACHE_TTL_MS
|
|
13
|
+
} from "../../lib/cache.js";
|
|
6
14
|
const metadata = {
|
|
7
15
|
GET: { requireAuth: true, requireFeatures: ["inbox_ops.settings.manage"] },
|
|
8
16
|
PATCH: { requireAuth: true, requireFeatures: ["inbox_ops.settings.manage"] }
|
|
@@ -10,6 +18,14 @@ const metadata = {
|
|
|
10
18
|
async function GET(req) {
|
|
11
19
|
try {
|
|
12
20
|
const ctx = await resolveRequestContext(req);
|
|
21
|
+
const cache = resolveCache(ctx.container);
|
|
22
|
+
if (cache) {
|
|
23
|
+
const cacheKey = createSettingsCacheKey(ctx.tenantId);
|
|
24
|
+
const cached = await runWithCacheTenant(ctx.tenantId, () => cache.get(cacheKey));
|
|
25
|
+
if (cached) {
|
|
26
|
+
return NextResponse.json(cached);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
13
29
|
const settings = await findOneWithDecryption(
|
|
14
30
|
ctx.em,
|
|
15
31
|
InboxSettings,
|
|
@@ -21,14 +37,27 @@ async function GET(req) {
|
|
|
21
37
|
void 0,
|
|
22
38
|
ctx.scope
|
|
23
39
|
);
|
|
24
|
-
|
|
40
|
+
const responseBody = {
|
|
25
41
|
settings: settings ? {
|
|
26
42
|
id: settings.id,
|
|
27
43
|
inboxAddress: settings.inboxAddress,
|
|
28
44
|
isActive: settings.isActive,
|
|
29
45
|
workingLanguage: settings.workingLanguage
|
|
30
46
|
} : null
|
|
31
|
-
}
|
|
47
|
+
};
|
|
48
|
+
if (cache) {
|
|
49
|
+
const cacheKey = createSettingsCacheKey(ctx.tenantId);
|
|
50
|
+
const tag = createSettingsCacheTag(ctx.tenantId);
|
|
51
|
+
try {
|
|
52
|
+
await runWithCacheTenant(
|
|
53
|
+
ctx.tenantId,
|
|
54
|
+
() => cache.set(cacheKey, responseBody, { ttl: SETTINGS_CACHE_TTL_MS, tags: [tag] })
|
|
55
|
+
);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.warn("[inbox_ops:settings] Failed to set cache", err);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return NextResponse.json(responseBody);
|
|
32
61
|
} catch (err) {
|
|
33
62
|
return handleRouteError(err, "load settings");
|
|
34
63
|
}
|
|
@@ -62,6 +91,8 @@ async function PATCH(req) {
|
|
|
62
91
|
settings.isActive = parsed.data.isActive;
|
|
63
92
|
}
|
|
64
93
|
await ctx.em.flush();
|
|
94
|
+
const cache = resolveCache(ctx.container);
|
|
95
|
+
await runWithCacheTenant(ctx.tenantId, () => invalidateSettingsCache(cache, ctx.tenantId));
|
|
65
96
|
return NextResponse.json({
|
|
66
97
|
ok: true,
|
|
67
98
|
settings: {
|