@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.
Files changed (110) hide show
  1. package/dist/generated/entities/inbox_proposal/index.js +2 -0
  2. package/dist/generated/entities/inbox_proposal/index.js.map +2 -2
  3. package/dist/modules/catalog/inbox-actions.js +49 -0
  4. package/dist/modules/catalog/inbox-actions.js.map +2 -2
  5. package/dist/modules/customers/inbox-actions.js +69 -27
  6. package/dist/modules/customers/inbox-actions.js.map +3 -3
  7. package/dist/modules/inbox_ops/ai-tools.js +346 -0
  8. package/dist/modules/inbox_ops/ai-tools.js.map +7 -0
  9. package/dist/modules/inbox_ops/api/extract/route.js +3 -2
  10. package/dist/modules/inbox_ops/api/extract/route.js.map +2 -2
  11. package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js +4 -0
  12. package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js.map +2 -2
  13. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/accept/route.js +4 -0
  14. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/accept/route.js.map +2 -2
  15. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/complete/route.js +4 -0
  16. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/complete/route.js.map +2 -2
  17. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/reject/route.js +4 -0
  18. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/reject/route.js.map +2 -2
  19. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +4 -0
  20. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
  21. package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js +59 -0
  22. package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js.map +7 -0
  23. package/dist/modules/inbox_ops/api/proposals/[id]/reject/route.js +4 -0
  24. package/dist/modules/inbox_ops/api/proposals/[id]/reject/route.js.map +2 -2
  25. package/dist/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.js +34 -14
  26. package/dist/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.js.map +2 -2
  27. package/dist/modules/inbox_ops/api/proposals/counts/route.js +49 -4
  28. package/dist/modules/inbox_ops/api/proposals/counts/route.js.map +2 -2
  29. package/dist/modules/inbox_ops/api/proposals/route.js +13 -0
  30. package/dist/modules/inbox_ops/api/proposals/route.js.map +2 -2
  31. package/dist/modules/inbox_ops/api/settings/route.js +33 -2
  32. package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
  33. package/dist/modules/inbox_ops/backend/inbox-ops/page.js +28 -3
  34. package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
  35. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js +103 -5
  36. package/dist/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.js.map +2 -2
  37. package/dist/modules/inbox_ops/components/messages/InboxEmailContent.js +24 -0
  38. package/dist/modules/inbox_ops/components/messages/InboxEmailContent.js.map +7 -0
  39. package/dist/modules/inbox_ops/components/messages/InboxEmailPreview.js +29 -0
  40. package/dist/modules/inbox_ops/components/messages/InboxEmailPreview.js.map +7 -0
  41. package/dist/modules/inbox_ops/components/proposals/CategoryBadge.js +59 -0
  42. package/dist/modules/inbox_ops/components/proposals/CategoryBadge.js.map +7 -0
  43. package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js +3 -1
  44. package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js.map +2 -2
  45. package/dist/modules/inbox_ops/data/entities.js +4 -0
  46. package/dist/modules/inbox_ops/data/entities.js.map +2 -2
  47. package/dist/modules/inbox_ops/data/validators.js +30 -5
  48. package/dist/modules/inbox_ops/data/validators.js.map +2 -2
  49. package/dist/modules/inbox_ops/lib/cache.js +53 -0
  50. package/dist/modules/inbox_ops/lib/cache.js.map +7 -0
  51. package/dist/modules/inbox_ops/lib/contactValidation.js +38 -3
  52. package/dist/modules/inbox_ops/lib/contactValidation.js.map +2 -2
  53. package/dist/modules/inbox_ops/lib/executionHelpers.js +28 -1
  54. package/dist/modules/inbox_ops/lib/executionHelpers.js.map +2 -2
  55. package/dist/modules/inbox_ops/lib/extractionPrompt.js +2 -1
  56. package/dist/modules/inbox_ops/lib/extractionPrompt.js.map +2 -2
  57. package/dist/modules/inbox_ops/lib/messageObjectPreviews.js +52 -0
  58. package/dist/modules/inbox_ops/lib/messageObjectPreviews.js.map +7 -0
  59. package/dist/modules/inbox_ops/lib/messagesIntegration.js +155 -0
  60. package/dist/modules/inbox_ops/lib/messagesIntegration.js.map +7 -0
  61. package/dist/modules/inbox_ops/message-objects.js +36 -0
  62. package/dist/modules/inbox_ops/message-objects.js.map +7 -0
  63. package/dist/modules/inbox_ops/message-types.js +38 -0
  64. package/dist/modules/inbox_ops/message-types.js.map +7 -0
  65. package/dist/modules/inbox_ops/migrations/Migration20260303173020.js +13 -0
  66. package/dist/modules/inbox_ops/migrations/Migration20260303173020.js.map +7 -0
  67. package/dist/modules/inbox_ops/migrations/Migration20260303173215.js +15 -0
  68. package/dist/modules/inbox_ops/migrations/Migration20260303173215.js.map +7 -0
  69. package/dist/modules/inbox_ops/search.js +5 -3
  70. package/dist/modules/inbox_ops/search.js.map +2 -2
  71. package/dist/modules/inbox_ops/subscribers/extractionWorker.js +65 -3
  72. package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +2 -2
  73. package/generated/entities/inbox_proposal/index.ts +1 -0
  74. package/package.json +3 -3
  75. package/src/modules/catalog/inbox-actions.ts +55 -0
  76. package/src/modules/customers/inbox-actions.ts +86 -27
  77. package/src/modules/inbox_ops/ai-tools.ts +451 -0
  78. package/src/modules/inbox_ops/api/extract/route.ts +3 -2
  79. package/src/modules/inbox_ops/api/proposals/[id]/accept-all/route.ts +5 -0
  80. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/accept/route.ts +5 -0
  81. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/complete/route.ts +5 -0
  82. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/reject/route.ts +5 -0
  83. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +5 -0
  84. package/src/modules/inbox_ops/api/proposals/[id]/categorize/route.ts +61 -0
  85. package/src/modules/inbox_ops/api/proposals/[id]/reject/route.ts +5 -0
  86. package/src/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.ts +36 -16
  87. package/src/modules/inbox_ops/api/proposals/counts/route.ts +60 -5
  88. package/src/modules/inbox_ops/api/proposals/route.ts +14 -1
  89. package/src/modules/inbox_ops/api/settings/route.ts +36 -2
  90. package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +31 -3
  91. package/src/modules/inbox_ops/backend/inbox-ops/proposals/[id]/page.tsx +103 -1
  92. package/src/modules/inbox_ops/components/messages/InboxEmailContent.tsx +45 -0
  93. package/src/modules/inbox_ops/components/messages/InboxEmailPreview.tsx +40 -0
  94. package/src/modules/inbox_ops/components/proposals/CategoryBadge.tsx +59 -0
  95. package/src/modules/inbox_ops/components/proposals/EditActionDialog.tsx +3 -1
  96. package/src/modules/inbox_ops/components/proposals/types.ts +1 -0
  97. package/src/modules/inbox_ops/data/entities.ts +14 -1
  98. package/src/modules/inbox_ops/data/validators.ts +41 -5
  99. package/src/modules/inbox_ops/lib/cache.ts +60 -0
  100. package/src/modules/inbox_ops/lib/contactValidation.ts +31 -2
  101. package/src/modules/inbox_ops/lib/executionHelpers.ts +40 -0
  102. package/src/modules/inbox_ops/lib/extractionPrompt.ts +2 -1
  103. package/src/modules/inbox_ops/lib/messageObjectPreviews.ts +61 -0
  104. package/src/modules/inbox_ops/lib/messagesIntegration.ts +231 -0
  105. package/src/modules/inbox_ops/message-objects.ts +34 -0
  106. package/src/modules/inbox_ops/message-types.ts +36 -0
  107. package/src/modules/inbox_ops/migrations/Migration20260303173020.ts +13 -0
  108. package/src/modules/inbox_ops/migrations/Migration20260303173215.ts +15 -0
  109. package/src/modules/inbox_ops/search.ts +5 -3
  110. 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, inReplyToMessageId, references: payloadReferences } = payloadResult.data;
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({ ok: true, sentMessageId });
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 via the configured email provider",
123
- description: "Sends the draft_reply action payload as an email. Sets In-Reply-To and References headers for threading.",
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 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 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, subject, body, inReplyToMessageId, references: payloadReferences } = payloadResult.data\n\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\n action.metadata = {\n ...(action.metadata && typeof action.metadata === 'object' ? action.metadata : {}),\n replySentAt: new Date().toISOString(),\n sentMessageId,\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 })\n } catch (eventError) {\n console.error('[inbox_ops:reply:send] Failed to emit event:', eventError)\n }\n\n return NextResponse.json({ ok: true, sentMessageId })\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 via the configured email provider',\n description: 'Sends the draft_reply action payload as an email. 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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",
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
- return NextResponse.json({ pending, partial, accepted, rejected });
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 return NextResponse.json({ pending, partial, accepted, rejected })\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 counts',\n description: 'Returns counts by status for tab badges',\n responses: [\n { status: 200, description: 'Status counts object' },\n ],\n },\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB,yBAAyB;AAElD,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,0BAA0B,EAAE;AAC1E;AAEA,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,MAAM,MAAM,sBAAsB,GAAG;AAE3C,UAAM,QAAQ;AAAA,MACZ,gBAAgB,IAAI;AAAA,MACpB,UAAU,IAAI;AAAA,MACd,WAAW;AAAA,MACX,UAAU;AAAA,IACZ;AAIA,UAAM,CAAC,SAAS,SAAS,UAAU,QAAQ,IAAI,MAAM,QAAQ,IAAI;AAAA,MAC/D,IAAI,GAAG,MAAM,eAAe,EAAE,GAAG,OAAO,QAAQ,UAAU,CAAC;AAAA,MAC3D,IAAI,GAAG,MAAM,eAAe,EAAE,GAAG,OAAO,QAAQ,UAAU,CAAC;AAAA,MAC3D,IAAI,GAAG,MAAM,eAAe,EAAE,GAAG,OAAO,QAAQ,WAAW,CAAC;AAAA,MAC5D,IAAI,GAAG,MAAM,eAAe,EAAE,GAAG,OAAO,QAAQ,WAAW,CAAC;AAAA,IAC9D,CAAC;AAED,WAAO,aAAa,KAAK,EAAE,SAAS,SAAS,UAAU,SAAS,CAAC;AAAA,EACnE,SAAS,KAAK;AACZ,QAAI,eAAe,mBAAmB;AACpC,aAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrE;AACA,YAAQ,MAAM,uCAAuC,GAAG;AACxD,WAAO,aAAa,KAAK,EAAE,OAAO,uBAAuB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC7E;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,uBAAuB;AAAA,MACrD;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 { 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": "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",
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
- return NextResponse.json({
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: {