@open-mercato/core 0.10.1-develop.7922.1.417ebe275c → 0.10.1-develop.7925.1.ec40f75262

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 (43) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/catalog/backend/catalog/products/create/page.js +16 -4
  3. package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
  4. package/dist/modules/catalog/components/products/ProductMediaManager.js +5 -2
  5. package/dist/modules/catalog/components/products/ProductMediaManager.js.map +2 -2
  6. package/dist/modules/entities/api/records.js +18 -2
  7. package/dist/modules/entities/api/records.js.map +2 -2
  8. package/dist/modules/staff/api/timesheets/time-entries/route.js +65 -5
  9. package/dist/modules/staff/api/timesheets/time-entries/route.js.map +2 -2
  10. package/dist/modules/staff/backend/staff/time-tracking/entries/page.js +49 -25
  11. package/dist/modules/staff/backend/staff/time-tracking/entries/page.js.map +2 -2
  12. package/dist/modules/staff/lib/time-tracking-ui/TimeEntriesSummaryFooter.js +13 -6
  13. package/dist/modules/staff/lib/time-tracking-ui/TimeEntriesSummaryFooter.js.map +2 -2
  14. package/dist/modules/staff/lib/time-tracking-ui/timeEntryListData.js +23 -0
  15. package/dist/modules/staff/lib/time-tracking-ui/timeEntryListData.js.map +2 -2
  16. package/dist/modules/staff/lib/timesheets/timeEntryTotals.js +149 -0
  17. package/dist/modules/staff/lib/timesheets/timeEntryTotals.js.map +7 -0
  18. package/dist/modules/staff/widgets/injection/time-entries-summary-footer/widget.js +24 -0
  19. package/dist/modules/staff/widgets/injection/time-entries-summary-footer/widget.js.map +7 -0
  20. package/dist/modules/staff/widgets/injection-table.js +4 -0
  21. package/dist/modules/staff/widgets/injection-table.js.map +2 -2
  22. package/package.json +7 -7
  23. package/src/modules/catalog/backend/catalog/products/create/page.tsx +14 -1
  24. package/src/modules/catalog/components/products/ProductMediaManager.tsx +5 -1
  25. package/src/modules/entities/api/records.ts +18 -2
  26. package/src/modules/entities/i18n/de.json +1 -0
  27. package/src/modules/entities/i18n/en.json +1 -0
  28. package/src/modules/entities/i18n/es.json +1 -0
  29. package/src/modules/entities/i18n/ko.json +1 -0
  30. package/src/modules/entities/i18n/pl.json +1 -0
  31. package/src/modules/staff/AGENTS.md +1 -0
  32. package/src/modules/staff/api/timesheets/time-entries/route.ts +77 -4
  33. package/src/modules/staff/backend/staff/time-tracking/entries/page.tsx +29 -7
  34. package/src/modules/staff/i18n/de.json +1 -0
  35. package/src/modules/staff/i18n/en.json +1 -0
  36. package/src/modules/staff/i18n/es.json +1 -0
  37. package/src/modules/staff/i18n/ko.json +1 -0
  38. package/src/modules/staff/i18n/pl.json +1 -0
  39. package/src/modules/staff/lib/time-tracking-ui/TimeEntriesSummaryFooter.tsx +25 -8
  40. package/src/modules/staff/lib/time-tracking-ui/timeEntryListData.ts +30 -0
  41. package/src/modules/staff/lib/timesheets/timeEntryTotals.ts +271 -0
  42. package/src/modules/staff/widgets/injection/time-entries-summary-footer/widget.tsx +32 -0
  43. package/src/modules/staff/widgets/injection-table.ts +4 -0
@@ -24,7 +24,8 @@ function ProductMediaManager({
24
24
  items,
25
25
  defaultMediaId,
26
26
  onItemsChange,
27
- onDefaultChange
27
+ onDefaultChange,
28
+ onUploadingChange
28
29
  }) {
29
30
  const t = useT();
30
31
  const fileInputRef = React.useRef(null);
@@ -36,6 +37,7 @@ function ProductMediaManager({
36
37
  if (!files || !files.length || !draftRecordId) return;
37
38
  setError(null);
38
39
  setUploading(true);
40
+ onUploadingChange?.(true);
39
41
  let nextItems = items;
40
42
  let defaultId = defaultMediaId;
41
43
  try {
@@ -78,12 +80,13 @@ function ProductMediaManager({
78
80
  }
79
81
  } finally {
80
82
  setUploading(false);
83
+ onUploadingChange?.(false);
81
84
  if (fileInputRef.current) {
82
85
  fileInputRef.current.value = "";
83
86
  }
84
87
  }
85
88
  },
86
- [defaultMediaId, draftRecordId, entityId, items, onDefaultChange, onItemsChange, t]
89
+ [defaultMediaId, draftRecordId, entityId, items, onDefaultChange, onItemsChange, onUploadingChange, t]
87
90
  );
88
91
  const handleRemove = React.useCallback(
89
92
  async (attachmentId) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/catalog/components/products/ProductMediaManager.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Upload, Image as ImageIcon, Trash2, Star } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'\n\nexport type ProductMediaItem = {\n id: string\n url: string\n fileName: string\n fileSize: number\n thumbnailUrl?: string | null\n}\n\ntype Props = {\n entityId: string\n draftRecordId: string\n items: ProductMediaItem[]\n defaultMediaId: string | null\n onItemsChange: (items: ProductMediaItem[]) => void\n onDefaultChange: (attachmentId: string | null) => void\n}\n\nfunction humanFileSize(size: number): string {\n if (!Number.isFinite(size)) return `${size}`\n const units = ['B', 'KB', 'MB', 'GB']\n let idx = 0\n let value = size\n while (value >= 1024 && idx < units.length - 1) {\n value /= 1024\n idx++\n }\n return `${value.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`\n}\n\nexport function ProductMediaManager({\n entityId,\n draftRecordId,\n items,\n defaultMediaId,\n onItemsChange,\n onDefaultChange,\n}: Props) {\n const t = useT()\n const fileInputRef = React.useRef<HTMLInputElement | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n const [isUploading, setUploading] = React.useState(false)\n const [isDragOver, setDragOver] = React.useState(false)\n\n const acceptFiles = React.useCallback(\n async (files: FileList | null) => {\n if (!files || !files.length || !draftRecordId) return\n setError(null)\n setUploading(true)\n let nextItems = items\n let defaultId = defaultMediaId\n try {\n for (const file of Array.from(files)) {\n if (!file.type.startsWith('image/')) {\n setError(t('catalog.products.media.errors.imagesOnly', 'Only image files are supported.'))\n continue\n }\n const fd = new FormData()\n fd.set('entityId', entityId)\n fd.set('recordId', draftRecordId)\n fd.set('file', file)\n // optimistic-lock-exempt: media attach/detach/reorder\n const call = await apiCall<{ ok?: boolean; item?: ProductMediaItem; error?: string }>(\n '/api/attachments',\n { method: 'POST', body: fd },\n { fallback: null },\n )\n if (!call.ok || !call.result?.item) {\n const message = call.result?.error ?? t('catalog.products.media.errors.uploadFailed', 'Upload failed.')\n setError(message)\n break\n }\n const existingIds = new Set(nextItems.map((entry) => entry.id))\n if (existingIds.has(call.result.item.id)) continue\n const slug = slugifyAttachmentFileName(call.result.item.fileName)\n const uploaded = {\n ...call.result.item,\n thumbnailUrl:\n call.result.item.thumbnailUrl ??\n buildAttachmentImageUrl(call.result.item.id, { width: 360, height: 360, slug }),\n }\n nextItems = [...nextItems, uploaded]\n if (!defaultId) {\n defaultId = uploaded.id\n }\n }\n if (nextItems !== items) {\n onItemsChange(nextItems)\n }\n if (!defaultMediaId && defaultId) {\n onDefaultChange(defaultId)\n }\n } finally {\n setUploading(false)\n if (fileInputRef.current) {\n fileInputRef.current.value = ''\n }\n }\n },\n [defaultMediaId, draftRecordId, entityId, items, onDefaultChange, onItemsChange, t],\n )\n\n const handleRemove = React.useCallback(\n async (attachmentId: string) => {\n setError(null)\n // optimistic-lock-exempt: media attach/detach/reorder\n const call = await apiCall<{ ok?: boolean; error?: string }>(\n `/api/attachments?id=${encodeURIComponent(attachmentId)}`,\n { method: 'DELETE' },\n { fallback: null },\n )\n if (!call.ok) {\n setError(call.result?.error ?? t('catalog.products.media.errors.deleteFailed', 'Failed to delete media.'))\n return\n }\n const next = items.filter((item) => item.id !== attachmentId)\n onItemsChange(next)\n if (defaultMediaId === attachmentId) {\n onDefaultChange(next[0]?.id ?? null)\n }\n },\n [defaultMediaId, items, onDefaultChange, onItemsChange, t],\n )\n\n const onDrop = React.useCallback(\n (event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setDragOver(false)\n void acceptFiles(event.dataTransfer?.files ?? null)\n },\n [acceptFiles],\n )\n\n const onDragOver = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setDragOver(true)\n }, [])\n\n const onDragLeave = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setDragOver(false)\n }, [])\n\n const pickFiles = React.useCallback(() => {\n fileInputRef.current?.click()\n }, [])\n\n return (\n <div className=\"space-y-3\">\n <label className=\"text-sm font-medium\">{t('catalog.products.media.label', 'Media')}</label>\n <div\n className={cn(\n 'flex flex-col items-center justify-center rounded-lg border border-dashed p-6 text-center transition-colors',\n isDragOver ? 'border-primary bg-primary/5' : 'border-muted-foreground/30',\n )}\n onDrop={onDrop}\n onDragOver={onDragOver}\n onDragLeave={onDragLeave}\n role=\"presentation\"\n >\n <Upload className=\"mx-auto h-6 w-6 text-muted-foreground\" />\n <p className=\"mt-2 text-sm text-muted-foreground\">\n {t('catalog.products.media.dropHint', 'Drag and drop images here or click to upload.')}\n </p>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"mt-4\" onClick={pickFiles} disabled={isUploading}>\n {isUploading ? t('catalog.products.media.uploading', 'Uploading\u2026') : t('catalog.products.media.choose', 'Choose files')}\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\n className=\"hidden\"\n onChange={(event) => void acceptFiles(event.target.files)}\n />\n </div>\n {error ? <p className=\"text-xs font-medium text-status-error-text\">{error}</p> : null}\n {items.length ? (\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3\">\n {items.map((item) => {\n const isDefault = defaultMediaId === item.id\n const slug = slugifyAttachmentFileName(item.fileName)\n const thumbnail =\n item.thumbnailUrl ||\n buildAttachmentImageUrl(item.id, { width: 360, height: 360, slug })\n return (\n <div key={item.id} className=\"flex flex-col rounded-md border bg-card\">\n <div className=\"relative aspect-square overflow-hidden rounded-t-md bg-muted\">\n {thumbnail ? (\n <img src={thumbnail} alt={item.fileName} className=\"h-full w-full object-cover\" />\n ) : (\n <div className=\"flex h-full items-center justify-center text-muted-foreground\">\n <ImageIcon className=\"h-8 w-8\" />\n </div>\n )}\n <div className=\"absolute right-1 top-1 flex gap-1\">\n <Button\n type=\"button\"\n variant={isDefault ? 'default' : 'secondary'}\n size=\"icon\"\n className={cn('h-7 w-7', isDefault ? '' : 'opacity-80')}\n onClick={() => onDefaultChange(item.id)}\n >\n <Star className=\"h-3.5 w-3.5\" />\n </Button>\n <Button type=\"button\" variant=\"secondary\" size=\"icon\" className=\"h-7 w-7\" onClick={() => void handleRemove(item.id)}>\n <Trash2 className=\"h-3.5 w-3.5\" />\n </Button>\n </div>\n </div>\n <div className=\"p-2\">\n <p className=\"line-clamp-1 text-sm font-medium\">{item.fileName}</p>\n <p className=\"text-xs text-muted-foreground\">{humanFileSize(item.fileSize)}</p>\n {isDefault ? (\n <p className=\"text-xs font-semibold text-primary\">\n {t('catalog.products.media.default', 'Default preview')}\n </p>\n ) : null}\n </div>\n </div>\n )\n })}\n </div>\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n {t('catalog.products.media.empty', 'No media uploaded yet.')}\n </p>\n )}\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Upload, Image as ImageIcon, Trash2, Star } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'\n\nexport type ProductMediaItem = {\n id: string\n url: string\n fileName: string\n fileSize: number\n thumbnailUrl?: string | null\n}\n\ntype Props = {\n entityId: string\n draftRecordId: string\n items: ProductMediaItem[]\n defaultMediaId: string | null\n onItemsChange: (items: ProductMediaItem[]) => void\n onDefaultChange: (attachmentId: string | null) => void\n onUploadingChange?: (isUploading: boolean) => void\n}\n\nfunction humanFileSize(size: number): string {\n if (!Number.isFinite(size)) return `${size}`\n const units = ['B', 'KB', 'MB', 'GB']\n let idx = 0\n let value = size\n while (value >= 1024 && idx < units.length - 1) {\n value /= 1024\n idx++\n }\n return `${value.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`\n}\n\nexport function ProductMediaManager({\n entityId,\n draftRecordId,\n items,\n defaultMediaId,\n onItemsChange,\n onDefaultChange,\n onUploadingChange,\n}: Props) {\n const t = useT()\n const fileInputRef = React.useRef<HTMLInputElement | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n const [isUploading, setUploading] = React.useState(false)\n const [isDragOver, setDragOver] = React.useState(false)\n\n const acceptFiles = React.useCallback(\n async (files: FileList | null) => {\n if (!files || !files.length || !draftRecordId) return\n setError(null)\n setUploading(true)\n onUploadingChange?.(true)\n let nextItems = items\n let defaultId = defaultMediaId\n try {\n for (const file of Array.from(files)) {\n if (!file.type.startsWith('image/')) {\n setError(t('catalog.products.media.errors.imagesOnly', 'Only image files are supported.'))\n continue\n }\n const fd = new FormData()\n fd.set('entityId', entityId)\n fd.set('recordId', draftRecordId)\n fd.set('file', file)\n // optimistic-lock-exempt: media attach/detach/reorder\n const call = await apiCall<{ ok?: boolean; item?: ProductMediaItem; error?: string }>(\n '/api/attachments',\n { method: 'POST', body: fd },\n { fallback: null },\n )\n if (!call.ok || !call.result?.item) {\n const message = call.result?.error ?? t('catalog.products.media.errors.uploadFailed', 'Upload failed.')\n setError(message)\n break\n }\n const existingIds = new Set(nextItems.map((entry) => entry.id))\n if (existingIds.has(call.result.item.id)) continue\n const slug = slugifyAttachmentFileName(call.result.item.fileName)\n const uploaded = {\n ...call.result.item,\n thumbnailUrl:\n call.result.item.thumbnailUrl ??\n buildAttachmentImageUrl(call.result.item.id, { width: 360, height: 360, slug }),\n }\n nextItems = [...nextItems, uploaded]\n if (!defaultId) {\n defaultId = uploaded.id\n }\n }\n if (nextItems !== items) {\n onItemsChange(nextItems)\n }\n if (!defaultMediaId && defaultId) {\n onDefaultChange(defaultId)\n }\n } finally {\n setUploading(false)\n onUploadingChange?.(false)\n if (fileInputRef.current) {\n fileInputRef.current.value = ''\n }\n }\n },\n [defaultMediaId, draftRecordId, entityId, items, onDefaultChange, onItemsChange, onUploadingChange, t],\n )\n\n const handleRemove = React.useCallback(\n async (attachmentId: string) => {\n setError(null)\n // optimistic-lock-exempt: media attach/detach/reorder\n const call = await apiCall<{ ok?: boolean; error?: string }>(\n `/api/attachments?id=${encodeURIComponent(attachmentId)}`,\n { method: 'DELETE' },\n { fallback: null },\n )\n if (!call.ok) {\n setError(call.result?.error ?? t('catalog.products.media.errors.deleteFailed', 'Failed to delete media.'))\n return\n }\n const next = items.filter((item) => item.id !== attachmentId)\n onItemsChange(next)\n if (defaultMediaId === attachmentId) {\n onDefaultChange(next[0]?.id ?? null)\n }\n },\n [defaultMediaId, items, onDefaultChange, onItemsChange, t],\n )\n\n const onDrop = React.useCallback(\n (event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setDragOver(false)\n void acceptFiles(event.dataTransfer?.files ?? null)\n },\n [acceptFiles],\n )\n\n const onDragOver = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setDragOver(true)\n }, [])\n\n const onDragLeave = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setDragOver(false)\n }, [])\n\n const pickFiles = React.useCallback(() => {\n fileInputRef.current?.click()\n }, [])\n\n return (\n <div className=\"space-y-3\">\n <label className=\"text-sm font-medium\">{t('catalog.products.media.label', 'Media')}</label>\n <div\n className={cn(\n 'flex flex-col items-center justify-center rounded-lg border border-dashed p-6 text-center transition-colors',\n isDragOver ? 'border-primary bg-primary/5' : 'border-muted-foreground/30',\n )}\n onDrop={onDrop}\n onDragOver={onDragOver}\n onDragLeave={onDragLeave}\n role=\"presentation\"\n >\n <Upload className=\"mx-auto h-6 w-6 text-muted-foreground\" />\n <p className=\"mt-2 text-sm text-muted-foreground\">\n {t('catalog.products.media.dropHint', 'Drag and drop images here or click to upload.')}\n </p>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"mt-4\" onClick={pickFiles} disabled={isUploading}>\n {isUploading ? t('catalog.products.media.uploading', 'Uploading\u2026') : t('catalog.products.media.choose', 'Choose files')}\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\n className=\"hidden\"\n onChange={(event) => void acceptFiles(event.target.files)}\n />\n </div>\n {error ? <p className=\"text-xs font-medium text-status-error-text\">{error}</p> : null}\n {items.length ? (\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3\">\n {items.map((item) => {\n const isDefault = defaultMediaId === item.id\n const slug = slugifyAttachmentFileName(item.fileName)\n const thumbnail =\n item.thumbnailUrl ||\n buildAttachmentImageUrl(item.id, { width: 360, height: 360, slug })\n return (\n <div key={item.id} className=\"flex flex-col rounded-md border bg-card\">\n <div className=\"relative aspect-square overflow-hidden rounded-t-md bg-muted\">\n {thumbnail ? (\n <img src={thumbnail} alt={item.fileName} className=\"h-full w-full object-cover\" />\n ) : (\n <div className=\"flex h-full items-center justify-center text-muted-foreground\">\n <ImageIcon className=\"h-8 w-8\" />\n </div>\n )}\n <div className=\"absolute right-1 top-1 flex gap-1\">\n <Button\n type=\"button\"\n variant={isDefault ? 'default' : 'secondary'}\n size=\"icon\"\n className={cn('h-7 w-7', isDefault ? '' : 'opacity-80')}\n onClick={() => onDefaultChange(item.id)}\n >\n <Star className=\"h-3.5 w-3.5\" />\n </Button>\n <Button type=\"button\" variant=\"secondary\" size=\"icon\" className=\"h-7 w-7\" onClick={() => void handleRemove(item.id)}>\n <Trash2 className=\"h-3.5 w-3.5\" />\n </Button>\n </div>\n </div>\n <div className=\"p-2\">\n <p className=\"line-clamp-1 text-sm font-medium\">{item.fileName}</p>\n <p className=\"text-xs text-muted-foreground\">{humanFileSize(item.fileSize)}</p>\n {isDefault ? (\n <p className=\"text-xs font-semibold text-primary\">\n {t('catalog.products.media.default', 'Default preview')}\n </p>\n ) : null}\n </div>\n </div>\n )\n })}\n </div>\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n {t('catalog.products.media.empty', 'No media uploaded yet.')}\n </p>\n )}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -10,6 +10,7 @@ import { parseBooleanToken, parseBooleanWithDefault } from "@open-mercato/shared
10
10
  import { parseCommaSeparatedList } from "@open-mercato/shared/lib/string";
11
11
  import { enforceCommandOptimisticLockWithGuards } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
12
12
  import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
13
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
13
14
  import { assertEntityAclForRequest } from "../lib/entityAcl.js";
14
15
  import { classifyRecordsEntity } from "../lib/entityClassification.js";
15
16
  import { loadCustomFieldKinds } from "../lib/scoped-field-defs.js";
@@ -335,13 +336,23 @@ async function POST(req) {
335
336
  const { id } = await de.createCustomEntityRecord({
336
337
  entityId,
337
338
  recordId: normalizedRecordId,
339
+ createOnly: true,
338
340
  organizationId: targetOrgId,
339
341
  tenantId: auth.tenantId,
340
342
  values: norm
341
343
  });
342
344
  return NextResponse.json({ ok: true, item: { entityId, recordId: id } });
343
345
  } catch (e) {
344
- if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status });
346
+ if (isCrudHttpError(e)) {
347
+ if (e.body.code === "custom_entity_record_exists") {
348
+ const { translate } = await resolveTranslations();
349
+ return NextResponse.json({
350
+ ...e.body,
351
+ error: translate("entities.records.errors.alreadyExists", "A record with this ID already exists. Use PUT to update it.")
352
+ }, { status: e.status });
353
+ }
354
+ return NextResponse.json(e.body, { status: e.status });
355
+ }
345
356
  logger.error("Records POST failed", { err: e });
346
357
  return NextResponse.json({ error: "Internal server error" }, { status: 500 });
347
358
  }
@@ -541,7 +552,7 @@ const openApi = {
541
552
  },
542
553
  POST: {
543
554
  summary: "Create record",
544
- description: "Creates a record for the given entity. When `recordId` is omitted or not a UUID the data engine will generate one automatically.",
555
+ description: "Creates a record for the given entity. An existing recordId returns 409; use PUT to update a record. When recordId is omitted or not a UUID the data engine will generate one automatically.",
545
556
  requestBody: {
546
557
  contentType: "application/json",
547
558
  schema: postBodySchema
@@ -552,6 +563,11 @@ const openApi = {
552
563
  description: "Record created",
553
564
  schema: mutationResponseSchema
554
565
  },
566
+ {
567
+ status: 409,
568
+ description: "Record ID already exists in the current scope",
569
+ schema: errorSchema
570
+ },
555
571
  {
556
572
  status: 400,
557
573
  description: "Validation failure",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/entities/api/records.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport type { QueryEngine, QueryOptions, Where, Sort } from '@open-mercato/shared/lib/query/types'\nimport { normalizeExportFormat, serializeExport, defaultExportFilename, ensureColumns } from '@open-mercato/shared/lib/crud/exporters'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { resolveOrganizationScope, getSelectedOrganizationFromRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE } from '@open-mercato/shared/lib/data/engine'\nimport { parseBooleanToken, parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'\nimport { setRecordCustomFields } from '../lib/helpers'\nimport { CustomFieldValue } from '../data/entities'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { assertEntityAclForRequest } from '../lib/entityAcl'\nimport { classifyRecordsEntity } from '../lib/entityClassification'\nimport { loadCustomFieldKinds } from '../lib/scoped-field-defs'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\n// Kept exported from this path: the classifier used to be private to this file\n// and the workflows task-visibility resolver reuses it, so both callers share\n// one precedence chain instead of growing a second, drifting copy.\nexport {\n classifyRecordsEntity,\n isDeclaredCustomEntity,\n loadDeclaredCustomEntities,\n findScopedCustomEntity,\n} from '../lib/entityClassification'\nexport type {\n RecordsEntityScope,\n RecordsEntityKind,\n RecordsEntityClassification,\n} from '../lib/entityClassification'\n\nconst logger = createLogger('entities').child({ component: 'records' })\n\nconst CUSTOM_ENTITY_RECORD_RESOURCE_KIND = 'entities.record'\n\nfunction systemEntityRecordsRejection(entityId: string) {\n return NextResponse.json(\n { error: 'Records are available for custom entities only', code: SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, entityId },\n { status: 400 },\n )\n}\n\nasync function readCustomEntityRecordUpdatedAt(\n em: any,\n input: { entityType: string; entityId: string; organizationId: string | null },\n): Promise<string | null> {\n try {\n const db = em.getKysely()\n let query = db\n .selectFrom('custom_entities_storage' as any)\n .select(['updated_at' as any])\n .where('entity_type' as any, '=', input.entityType)\n .where('entity_id' as any, '=', input.entityId)\n query = input.organizationId === null\n ? query.where('organization_id' as any, 'is', null as any)\n : query.where('organization_id' as any, '=', input.organizationId)\n const row = await query.executeTakeFirst()\n const value = (row as any)?.updated_at\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'string' && value.length > 0) return value\n return null\n } catch {\n return null\n }\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['entities.records.view'] },\n POST: { requireAuth: true, requireFeatures: ['entities.records.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['entities.records.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['entities.records.manage'] },\n}\n\nconst DEFAULT_EXPORT_PAGE_SIZE = 1000\nconst EXPORT_MAX_PAGES = 1000\n\nconst listRecordsQuerySchema = z\n .object({\n entityId: z.string().min(1),\n page: z.coerce.number().int().min(1).optional(),\n pageSize: z.coerce.number().int().min(1).max(100).optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n search: z.string().optional(),\n searchFields: z.string().optional(),\n withDeleted: z.coerce.boolean().optional(),\n format: z.enum(['csv', 'json', 'xml', 'markdown']).optional(),\n exportScope: z.enum(['full']).optional(),\n export_scope: z.enum(['full']).optional(),\n all: z.coerce.boolean().optional(),\n full: z.coerce.boolean().optional(),\n })\n .passthrough()\n\nconst recordItemSchema = z.record(z.string(), z.any())\n\nconst listRecordsResponseSchema = z.object({\n items: z.array(recordItemSchema),\n total: z.number(),\n page: z.number(),\n pageSize: z.number(),\n totalPages: z.number(),\n totalIsCapped: z.boolean().optional(),\n})\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const entityId = url.searchParams.get('entityId') || ''\n if (!entityId) return NextResponse.json({ error: 'entityId is required' }, { status: 400 })\n\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const requestedExport = normalizeExportFormat(url.searchParams.get('format'))\n const exportScopeRaw = (url.searchParams.get('exportScope') || url.searchParams.get('export_scope') || '').toLowerCase()\n const exportFullRequested = requestedExport != null && (exportScopeRaw === 'full' || parseBooleanWithDefault(url.searchParams.get('full'), false))\n const exportAll = parseBooleanWithDefault(url.searchParams.get('all'), false)\n const noPagination = exportAll || requestedExport != null\n const page = noPagination ? 1 : Math.max(parseInt(url.searchParams.get('page') || '1', 10) || 1, 1)\n const basePageSize = Math.min(Math.max(parseInt(url.searchParams.get('pageSize') || '50', 10) || 50, 1), 100)\n const pageSize = noPagination ? Math.max(basePageSize, DEFAULT_EXPORT_PAGE_SIZE) : basePageSize\n const sortField = url.searchParams.get('sortField') || 'id'\n const sortDir = (url.searchParams.get('sortDir') || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc'\n const withDeleted = parseBooleanWithDefault(url.searchParams.get('withDeleted'), false)\n const searchTerm = (url.searchParams.get('search') || '').trim()\n const searchFields = parseCommaSeparatedList(url.searchParams.get('searchFields'))\n\n const qpEntries: Array<[string, string]> = []\n for (const [key, val] of url.searchParams.entries()) {\n if (['entityId','page','pageSize','sortField','sortDir','withDeleted','format','exportScope','export_scope','all','full','search','searchFields'].includes(key)) continue\n qpEntries.push([key, val])\n }\n\n try {\n const { resolve } = await createRequestContainer()\n const qe = resolve('queryEngine') as QueryEngine\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n let organizationIds: string[] | null = scope.filterIds\n // Module-declared custom entities (ce.ts) carry frozen system-style ids and are never\n // registered in `custom_entities`, so classification checks the declared registry plus\n // active registrations. System (table-backed) ids are rejected above; for the allowed\n // set `isCustomEntity` drives mapRow's cf_ stripping so the edit form reads back values.\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'view', isCustomEntity, isRestricted, rbac })\n if (organizationIds && organizationIds.length === 0) {\n return NextResponse.json({ items: [], total: 0, page, pageSize, totalPages: 0 })\n }\n // Doc storage keeps custom-field values as JSON, so a checkbox can arrive as the\n // string \"true\"/\"1\" and must be handed back to the form as a real boolean. That\n // parse is only ever correct for fields DECLARED boolean: applied blind it also\n // rewrites legitimate string values whose text happens to be a boolean token \u2014\n // a `select` option `no` came back as `false`, and the edit form then wrote that\n // boolean back over the stored option (#5791). Consult the declared kind instead.\n const needsFieldKinds = isCustomEntity || qpEntries.some(([key]) => key.startsWith('cf_'))\n const fieldKinds = needsFieldKinds\n ? await loadCustomFieldKinds(em, { entityId, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null, tenantId: auth.tenantId ?? null })\n : new Map<string, string>()\n const isBooleanField = (key: string) => fieldKinds.get(key) === 'boolean'\n const parseBooleanField = (key: string, value: unknown) => {\n if (typeof value !== 'string' || !isBooleanField(key)) return value\n const parsed = parseBooleanToken(value)\n return parsed === null ? value : parsed\n }\n const normalizeCustomEntityValue = (key: string, value: unknown) => {\n if (Array.isArray(value)) return value.map((entry) => parseBooleanField(key, entry))\n return parseBooleanField(key, value)\n }\n const mapRow = (row: any) => {\n if (!isCustomEntity || !row || typeof row !== 'object') return row\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(row)) {\n if (k.startsWith('cf_')) {\n const bare = k.replace(/^cf_/, '')\n out[bare] = normalizeCustomEntityValue(bare, v)\n } else out[k] = v\n }\n return out\n }\n const mapFullRow = (row: any) => {\n if (!row || typeof row !== 'object') return row\n return { ...(row as Record<string, unknown>) }\n }\n // Build filters with awareness of custom-entity mode\n const filtersObj: Where<any> = {}\n const buildFilter = (key: string, val: string, allowAnyKey: boolean) => {\n if (key.startsWith('cf_')) {\n if (key.endsWith('In')) {\n const base = key.slice(0, -2)\n const values = parseCommaSeparatedList(val)\n ;(filtersObj as any)[base] = { $in: values }\n } else {\n if (val.includes(',')) {\n const values = parseCommaSeparatedList(val)\n ;(filtersObj as any)[key] = { $in: values }\n } else {\n ;(filtersObj as any)[key] = parseBooleanField(key.replace(/^cf_/, ''), val)\n }\n }\n } else if (allowAnyKey) {\n if (val.includes(',')) {\n const values = parseCommaSeparatedList(val)\n ;(filtersObj as any)[key] = { $in: values }\n } else {\n ;(filtersObj as any)[key] = parseBooleanField(key, val)\n }\n } else {\n if (['id', 'created_at', 'updated_at', 'deleted_at', 'name', 'title', 'email'].includes(key)) {\n ;(filtersObj as any)[key] = val\n }\n }\n }\n\n if (organizationIds && organizationIds.length) {\n (filtersObj as any).organization_id = { $in: organizationIds }\n }\n const qopts: QueryOptions = {\n tenantId: auth.tenantId!,\n includeCustomFields: true,\n page: { page, pageSize },\n sort: [{ field: sortField as any, dir: sortDir as any }] as Sort[],\n filters: filtersObj as any,\n withDeleted,\n }\n if (organizationIds && organizationIds.length) {\n qopts.organizationIds = organizationIds\n }\n // Allowed entities are doc-storage-backed by definition (system ids were rejected\n // above) \u2014 direct the engine to doc storage explicitly so reads stay deterministic\n // even before the first record exists.\n if (isCustomEntity) qopts.forceCustomEntityStorage = true\n for (const [k, v] of qpEntries) buildFilter(k, v, isCustomEntity)\n // Server-side full-result search: match the term against the requested fields\n // (defaults to `id`) before pagination so totals/exports stay consistent with\n // the active search instead of filtering only the current client page (#3229).\n if (searchTerm) {\n const fields = searchFields.length ? searchFields : ['id']\n const pattern = `%${searchTerm}%`\n const orClauses = fields.map((field) => ({ [field]: { $ilike: pattern } }))\n ;(filtersObj as any).$or = orClauses\n }\n const res = await qe.query(entityId as any, qopts)\n const rawItems = res.items || []\n const viewPageItems = rawItems.map(mapRow)\n const fullPageItems = rawItems.map(mapFullRow)\n\n // Expose `updated_at` on custom-entity records. The query engine returns only\n // the `doc` fields + `id`, dropping the base `updated_at` column \u2014 which made\n // optimistic locking impossible end-to-end (no version for the edit page to\n // round-trip as the lock header). Batch-read it from storage and merge it in.\n if (isCustomEntity && viewPageItems.length) {\n try {\n const recordIds = viewPageItems\n .map((it: any) => it?.id)\n .filter((v: any): v is string => typeof v === 'string' && v.length > 0)\n if (recordIds.length) {\n const db = em.getKysely()\n const rows = await db\n .selectFrom('custom_entities_storage' as any)\n .select(['entity_id' as any, 'updated_at' as any])\n .where('entity_type' as any, '=', entityId)\n .where('entity_id' as any, 'in', recordIds as any)\n .execute()\n const updatedById = new Map<string, string>()\n for (const row of rows as any[]) {\n const value = row?.updated_at\n const iso = value instanceof Date ? value.toISOString() : (typeof value === 'string' && value.length > 0 ? value : null)\n if (iso && row?.entity_id) updatedById.set(String(row.entity_id), iso)\n }\n for (const item of viewPageItems as any[]) {\n const iso = updatedById.get(String(item?.id))\n if (iso) {\n item.updated_at = iso\n item.updatedAt = iso\n }\n }\n }\n } catch { /* best-effort: locking simply will not engage if storage is unavailable */ }\n }\n\n const total = typeof res.total === 'number' ? res.total : rawItems.length\n const effectivePageSize = res.pageSize || pageSize\n const payload = {\n items: viewPageItems,\n total,\n page: res.page || page,\n pageSize: effectivePageSize,\n totalPages: Math.ceil(total / (effectivePageSize || 1)),\n ...(res.meta?.listCountCapWarning ? { totalIsCapped: true } : {}),\n }\n\n if (requestedExport) {\n const exportItems: any[] = exportFullRequested ? [...fullPageItems] : [...viewPageItems]\n // Short-page termination: `total` is a display value, not a loop bound \u2014 it can\n // under-report (capped counts) or drift while rows are inserted/deleted mid-export.\n // Keep fetching while pages come back full; fail closed at the page ceiling rather\n // than serializing a partial export.\n if (rawItems.length >= pageSize) {\n let nextPage = 2\n for (;;) {\n const nextRes = await qe.query(entityId as any, {\n ...qopts,\n page: { page: nextPage, pageSize },\n })\n const nextRawItems = nextRes.items || []\n if (!nextRawItems.length) break\n const nextViewItems = nextRawItems.map(mapRow)\n const nextFullItems = nextRawItems.map(mapFullRow)\n const nextBatch = exportFullRequested ? nextFullItems : nextViewItems\n exportItems.push(...nextBatch)\n if (nextRawItems.length < pageSize) break\n if (nextPage >= EXPORT_MAX_PAGES) {\n throw new Error(`[internal] export exceeded ${EXPORT_MAX_PAGES} pages; refusing to return a partial export`)\n }\n nextPage += 1\n }\n }\n const prepared = {\n columns: ensureColumns(exportItems),\n rows: exportItems,\n }\n const filenameBase = exportFullRequested ? `${entityId || 'records'}_full` : entityId || 'records'\n const serialized = serializeExport(prepared, requestedExport)\n const filename = defaultExportFilename(filenameBase, requestedExport)\n return new Response(serialized.body, {\n headers: {\n 'content-type': serialized.contentType,\n 'content-disposition': `attachment; filename=\"${filename}\"`,\n },\n })\n }\n\n return NextResponse.json(payload)\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n logger.error('Records GET failed', { err: e })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nconst postBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1).optional(),\n values: z.record(z.string(), z.any()).default({}),\n})\n\nconst putBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1),\n values: z.record(z.string(), z.any()).default({}),\n})\n\nconst mutationResponseSchema = z.object({\n ok: z.literal(true),\n item: z\n .object({\n entityId: z.string(),\n recordId: z.string(),\n })\n .optional(),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let json: unknown\n try { json = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = postBodySchema.safeParse(json)\n if (!parsed.success) return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n const { entityId } = parsed.data\n let { recordId, values } = parsed.data as { recordId?: string; values: Record<string, any> }\n\n try {\n const { resolve } = await createRequestContainer()\n const de = resolve('dataEngine') as any\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n const targetOrgId = resolveSingleOrganizationIdOrDeny(scope, auth)\n if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, isRestricted, rbac })\n // Strip reserved record/system columns the edit form echoes back from the loaded record\n // (`id`, plus `updated_at`/`updatedAt` used for optimistic locking). They are not custom\n // fields; without this they validate as cf_id / cf_updated_at / cf_updatedAt and are\n // rejected as \"Unknown custom field\", which fails EVERY custom-entity edit-form save.\n for (const reservedKey of ['id', 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt']) {\n delete (values as any)[reservedKey]\n }\n const norm = normalizeValues(values)\n\n // Validate against custom field definitions\n try {\n const { validateCustomFieldValuesServer } = await import('../lib/validation')\n const check = await validateCustomFieldValuesServer(em, { entityId, organizationId: targetOrgId, tenantId: auth.tenantId!, values: norm, rejectUndeclaredKeys: true })\n if (!check.ok) return NextResponse.json({ error: 'Validation failed', fields: check.fieldErrors }, { status: 400 })\n } catch { /* ignore if helper missing */ }\n\n const normalizedRecordId = (() => {\n const raw = String(recordId || '').trim()\n if (!raw) return undefined\n const low = raw.toLowerCase()\n if (low === 'create' || low === 'new' || low === 'null' || low === 'undefined') return undefined\n // Enforce UUID only; any non-uuid is ignored so we generate one in the DE\n const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n return uuid.test(raw) ? raw : undefined\n })()\n const { id } = await de.createCustomEntityRecord({\n entityId,\n recordId: normalizedRecordId,\n organizationId: targetOrgId,\n tenantId: auth.tenantId!,\n values: norm,\n })\n\n return NextResponse.json({ ok: true, item: { entityId, recordId: id } })\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n logger.error('Records POST failed', { err: e })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\n// Avoid zod here to prevent runtime import issues in some environments\nfunction parsePutBody(json: any): { ok: true; data: { entityId: string; recordId: string; values: Record<string, any> } } | { ok: false; error: string } {\n if (!json || typeof json !== 'object') return { ok: false, error: 'Invalid JSON' }\n const entityId = typeof json.entityId === 'string' && json.entityId.length ? json.entityId : ''\n const recordId = typeof json.recordId === 'string' && json.recordId.length ? json.recordId : ''\n const values = (json.values && typeof json.values === 'object') ? json.values as Record<string, any> : {}\n if (!entityId || !recordId) return { ok: false, error: 'entityId and recordId are required' }\n return { ok: true, data: { entityId, recordId, values } }\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let json: any\n try { json = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = parsePutBody(json)\n if (!parsed.ok) return NextResponse.json({ error: parsed.error }, { status: 400 })\n const { entityId, recordId, values } = parsed.data\n\n try {\n const container = await createRequestContainer()\n const { resolve } = container\n const de = resolve('dataEngine') as any\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n const targetOrgId = resolveSingleOrganizationIdOrDeny(scope, auth)\n if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, isRestricted, rbac })\n // Strip reserved record/system columns the edit form echoes back from the loaded record\n // (`id`, plus `updated_at`/`updatedAt` used for optimistic locking). They are not custom\n // fields; without this they validate as cf_id / cf_updated_at / cf_updatedAt and are\n // rejected as \"Unknown custom field\", which fails EVERY custom-entity edit-form save.\n for (const reservedKey of ['id', 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt']) {\n delete (values as any)[reservedKey]\n }\n const norm = normalizeValues(values)\n\n // Validate against custom field definitions\n try {\n const { validateCustomFieldValuesServer } = await import('../lib/validation')\n const check = await validateCustomFieldValuesServer(em, { entityId, organizationId: targetOrgId, tenantId: auth.tenantId!, values: norm, rejectUndeclaredKeys: true })\n if (!check.ok) return NextResponse.json({ error: 'Validation failed', fields: check.fieldErrors }, { status: 400 })\n } catch { /* ignore if helper missing */ }\n\n // Normalize recordId: if blank/sentinel/non-uuid => create instead of update\n const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n const rid = String(recordId || '').trim()\n const low = rid.toLowerCase()\n const isSentinel = !rid || low === 'create' || low === 'new' || low === 'null' || low === 'undefined'\n const isUuid = uuidRe.test(rid)\n if (isSentinel || !isUuid) {\n const created = await de.createCustomEntityRecord({\n entityId,\n recordId: undefined,\n organizationId: targetOrgId,\n tenantId: auth.tenantId!,\n values: norm,\n })\n return NextResponse.json({ ok: true, item: { entityId, recordId: created.id } })\n }\n\n try {\n const currentUpdatedAt = await readCustomEntityRecordUpdatedAt(em, {\n entityType: entityId,\n entityId: rid,\n organizationId: targetOrgId,\n })\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: CUSTOM_ENTITY_RECORD_RESOURCE_KIND,\n resourceId: rid,\n current: currentUpdatedAt,\n request: req,\n })\n } catch (lockError) {\n if (isCrudHttpError(lockError)) {\n return NextResponse.json(lockError.body, { status: lockError.status })\n }\n throw lockError\n }\n\n await de.updateCustomEntityRecord({\n entityId,\n recordId: rid,\n organizationId: targetOrgId,\n tenantId: auth.tenantId!,\n values: norm,\n })\n return NextResponse.json({ ok: true, item: { entityId, recordId: rid } })\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nconst deleteBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1),\n})\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const qpEntityId = url.searchParams.get('entityId')\n const qpRecordId = url.searchParams.get('recordId')\n let payload: any = qpEntityId && qpRecordId ? { entityId: qpEntityId, recordId: qpRecordId } : null\n if (!payload) {\n try { payload = await req.json() } catch { payload = null }\n }\n const parsed = deleteBodySchema.safeParse(payload)\n if (!parsed.success) return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n const { entityId, recordId } = parsed.data\n\n try {\n const container = await createRequestContainer()\n const { resolve } = container\n const de = resolve('dataEngine') as any\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n const targetOrgId = resolveSingleOrganizationIdOrDeny(scope, auth)\n if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, isRestricted, rbac })\n\n try {\n const currentUpdatedAt = await readCustomEntityRecordUpdatedAt(em, {\n entityType: entityId,\n entityId: recordId,\n organizationId: targetOrgId,\n })\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: CUSTOM_ENTITY_RECORD_RESOURCE_KIND,\n resourceId: recordId,\n current: currentUpdatedAt,\n request: req,\n })\n } catch (lockError) {\n if (isCrudHttpError(lockError)) {\n return NextResponse.json(lockError.body, { status: lockError.status })\n }\n throw lockError\n }\n\n await de.deleteCustomEntityRecord({ entityId, recordId, organizationId: targetOrgId, tenantId: auth.tenantId!, soft: true })\n return NextResponse.json({ ok: true })\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nfunction normalizeValues(input: Record<string, any>): Record<string, any> {\n const out: Record<string, any> = {}\n for (const [k, v] of Object.entries(input || {})) {\n const key = k.startsWith('cf_') ? k.replace(/^cf_/, '') : k\n out[key] = v\n }\n return out\n}\n\nconst deleteResponseSchema = z.object({\n ok: z.literal(true),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n}).passthrough()\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'CRUD operations on entity records',\n methods: {\n GET: {\n summary: 'List records',\n description:\n 'Returns paginated records for the supplied entity. Supports custom field filters, exports, and soft-delete toggles.',\n query: listRecordsQuerySchema,\n responses: [\n {\n status: 200,\n description: 'Paginated records',\n schema: listRecordsResponseSchema,\n },\n {\n status: 400,\n description: 'Missing entity id',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n POST: {\n summary: 'Create record',\n description:\n 'Creates a record for the given entity. When `recordId` is omitted or not a UUID the data engine will generate one automatically.',\n requestBody: {\n contentType: 'application/json',\n schema: postBodySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Record created',\n schema: mutationResponseSchema,\n },\n {\n status: 400,\n description: 'Validation failure',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n PUT: {\n summary: 'Update record',\n description:\n 'Updates an existing record. If the provided recordId is not a UUID the record will be created instead to support optimistic flows.',\n requestBody: {\n contentType: 'application/json',\n schema: putBodySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Record updated',\n schema: mutationResponseSchema,\n },\n {\n status: 400,\n description: 'Validation failure',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n DELETE: {\n summary: 'Delete record',\n description: 'Soft deletes the specified record within the current tenant/org scope.',\n requestBody: {\n contentType: 'application/json',\n schema: deleteBodySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Record deleted',\n schema: deleteResponseSchema,\n },\n {\n status: 400,\n description: 'Missing entity id or record id',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 404,\n description: 'Record not found',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport type { QueryEngine, QueryOptions, Where, Sort } from '@open-mercato/shared/lib/query/types'\nimport { normalizeExportFormat, serializeExport, defaultExportFilename, ensureColumns } from '@open-mercato/shared/lib/crud/exporters'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { resolveOrganizationScope, getSelectedOrganizationFromRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE } from '@open-mercato/shared/lib/data/engine'\nimport { parseBooleanToken, parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'\nimport { setRecordCustomFields } from '../lib/helpers'\nimport { CustomFieldValue } from '../data/entities'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { assertEntityAclForRequest } from '../lib/entityAcl'\nimport { classifyRecordsEntity } from '../lib/entityClassification'\nimport { loadCustomFieldKinds } from '../lib/scoped-field-defs'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\n// Kept exported from this path: the classifier used to be private to this file\n// and the workflows task-visibility resolver reuses it, so both callers share\n// one precedence chain instead of growing a second, drifting copy.\nexport {\n classifyRecordsEntity,\n isDeclaredCustomEntity,\n loadDeclaredCustomEntities,\n findScopedCustomEntity,\n} from '../lib/entityClassification'\nexport type {\n RecordsEntityScope,\n RecordsEntityKind,\n RecordsEntityClassification,\n} from '../lib/entityClassification'\n\nconst logger = createLogger('entities').child({ component: 'records' })\n\nconst CUSTOM_ENTITY_RECORD_RESOURCE_KIND = 'entities.record'\n\nfunction systemEntityRecordsRejection(entityId: string) {\n return NextResponse.json(\n { error: 'Records are available for custom entities only', code: SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, entityId },\n { status: 400 },\n )\n}\n\nasync function readCustomEntityRecordUpdatedAt(\n em: any,\n input: { entityType: string; entityId: string; organizationId: string | null },\n): Promise<string | null> {\n try {\n const db = em.getKysely()\n let query = db\n .selectFrom('custom_entities_storage' as any)\n .select(['updated_at' as any])\n .where('entity_type' as any, '=', input.entityType)\n .where('entity_id' as any, '=', input.entityId)\n query = input.organizationId === null\n ? query.where('organization_id' as any, 'is', null as any)\n : query.where('organization_id' as any, '=', input.organizationId)\n const row = await query.executeTakeFirst()\n const value = (row as any)?.updated_at\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'string' && value.length > 0) return value\n return null\n } catch {\n return null\n }\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['entities.records.view'] },\n POST: { requireAuth: true, requireFeatures: ['entities.records.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['entities.records.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['entities.records.manage'] },\n}\n\nconst DEFAULT_EXPORT_PAGE_SIZE = 1000\nconst EXPORT_MAX_PAGES = 1000\n\nconst listRecordsQuerySchema = z\n .object({\n entityId: z.string().min(1),\n page: z.coerce.number().int().min(1).optional(),\n pageSize: z.coerce.number().int().min(1).max(100).optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n search: z.string().optional(),\n searchFields: z.string().optional(),\n withDeleted: z.coerce.boolean().optional(),\n format: z.enum(['csv', 'json', 'xml', 'markdown']).optional(),\n exportScope: z.enum(['full']).optional(),\n export_scope: z.enum(['full']).optional(),\n all: z.coerce.boolean().optional(),\n full: z.coerce.boolean().optional(),\n })\n .passthrough()\n\nconst recordItemSchema = z.record(z.string(), z.any())\n\nconst listRecordsResponseSchema = z.object({\n items: z.array(recordItemSchema),\n total: z.number(),\n page: z.number(),\n pageSize: z.number(),\n totalPages: z.number(),\n totalIsCapped: z.boolean().optional(),\n})\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const entityId = url.searchParams.get('entityId') || ''\n if (!entityId) return NextResponse.json({ error: 'entityId is required' }, { status: 400 })\n\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const requestedExport = normalizeExportFormat(url.searchParams.get('format'))\n const exportScopeRaw = (url.searchParams.get('exportScope') || url.searchParams.get('export_scope') || '').toLowerCase()\n const exportFullRequested = requestedExport != null && (exportScopeRaw === 'full' || parseBooleanWithDefault(url.searchParams.get('full'), false))\n const exportAll = parseBooleanWithDefault(url.searchParams.get('all'), false)\n const noPagination = exportAll || requestedExport != null\n const page = noPagination ? 1 : Math.max(parseInt(url.searchParams.get('page') || '1', 10) || 1, 1)\n const basePageSize = Math.min(Math.max(parseInt(url.searchParams.get('pageSize') || '50', 10) || 50, 1), 100)\n const pageSize = noPagination ? Math.max(basePageSize, DEFAULT_EXPORT_PAGE_SIZE) : basePageSize\n const sortField = url.searchParams.get('sortField') || 'id'\n const sortDir = (url.searchParams.get('sortDir') || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc'\n const withDeleted = parseBooleanWithDefault(url.searchParams.get('withDeleted'), false)\n const searchTerm = (url.searchParams.get('search') || '').trim()\n const searchFields = parseCommaSeparatedList(url.searchParams.get('searchFields'))\n\n const qpEntries: Array<[string, string]> = []\n for (const [key, val] of url.searchParams.entries()) {\n if (['entityId','page','pageSize','sortField','sortDir','withDeleted','format','exportScope','export_scope','all','full','search','searchFields'].includes(key)) continue\n qpEntries.push([key, val])\n }\n\n try {\n const { resolve } = await createRequestContainer()\n const qe = resolve('queryEngine') as QueryEngine\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n let organizationIds: string[] | null = scope.filterIds\n // Module-declared custom entities (ce.ts) carry frozen system-style ids and are never\n // registered in `custom_entities`, so classification checks the declared registry plus\n // active registrations. System (table-backed) ids are rejected above; for the allowed\n // set `isCustomEntity` drives mapRow's cf_ stripping so the edit form reads back values.\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'view', isCustomEntity, isRestricted, rbac })\n if (organizationIds && organizationIds.length === 0) {\n return NextResponse.json({ items: [], total: 0, page, pageSize, totalPages: 0 })\n }\n // Doc storage keeps custom-field values as JSON, so a checkbox can arrive as the\n // string \"true\"/\"1\" and must be handed back to the form as a real boolean. That\n // parse is only ever correct for fields DECLARED boolean: applied blind it also\n // rewrites legitimate string values whose text happens to be a boolean token \u2014\n // a `select` option `no` came back as `false`, and the edit form then wrote that\n // boolean back over the stored option (#5791). Consult the declared kind instead.\n const needsFieldKinds = isCustomEntity || qpEntries.some(([key]) => key.startsWith('cf_'))\n const fieldKinds = needsFieldKinds\n ? await loadCustomFieldKinds(em, { entityId, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null, tenantId: auth.tenantId ?? null })\n : new Map<string, string>()\n const isBooleanField = (key: string) => fieldKinds.get(key) === 'boolean'\n const parseBooleanField = (key: string, value: unknown) => {\n if (typeof value !== 'string' || !isBooleanField(key)) return value\n const parsed = parseBooleanToken(value)\n return parsed === null ? value : parsed\n }\n const normalizeCustomEntityValue = (key: string, value: unknown) => {\n if (Array.isArray(value)) return value.map((entry) => parseBooleanField(key, entry))\n return parseBooleanField(key, value)\n }\n const mapRow = (row: any) => {\n if (!isCustomEntity || !row || typeof row !== 'object') return row\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(row)) {\n if (k.startsWith('cf_')) {\n const bare = k.replace(/^cf_/, '')\n out[bare] = normalizeCustomEntityValue(bare, v)\n } else out[k] = v\n }\n return out\n }\n const mapFullRow = (row: any) => {\n if (!row || typeof row !== 'object') return row\n return { ...(row as Record<string, unknown>) }\n }\n // Build filters with awareness of custom-entity mode\n const filtersObj: Where<any> = {}\n const buildFilter = (key: string, val: string, allowAnyKey: boolean) => {\n if (key.startsWith('cf_')) {\n if (key.endsWith('In')) {\n const base = key.slice(0, -2)\n const values = parseCommaSeparatedList(val)\n ;(filtersObj as any)[base] = { $in: values }\n } else {\n if (val.includes(',')) {\n const values = parseCommaSeparatedList(val)\n ;(filtersObj as any)[key] = { $in: values }\n } else {\n ;(filtersObj as any)[key] = parseBooleanField(key.replace(/^cf_/, ''), val)\n }\n }\n } else if (allowAnyKey) {\n if (val.includes(',')) {\n const values = parseCommaSeparatedList(val)\n ;(filtersObj as any)[key] = { $in: values }\n } else {\n ;(filtersObj as any)[key] = parseBooleanField(key, val)\n }\n } else {\n if (['id', 'created_at', 'updated_at', 'deleted_at', 'name', 'title', 'email'].includes(key)) {\n ;(filtersObj as any)[key] = val\n }\n }\n }\n\n if (organizationIds && organizationIds.length) {\n (filtersObj as any).organization_id = { $in: organizationIds }\n }\n const qopts: QueryOptions = {\n tenantId: auth.tenantId!,\n includeCustomFields: true,\n page: { page, pageSize },\n sort: [{ field: sortField as any, dir: sortDir as any }] as Sort[],\n filters: filtersObj as any,\n withDeleted,\n }\n if (organizationIds && organizationIds.length) {\n qopts.organizationIds = organizationIds\n }\n // Allowed entities are doc-storage-backed by definition (system ids were rejected\n // above) \u2014 direct the engine to doc storage explicitly so reads stay deterministic\n // even before the first record exists.\n if (isCustomEntity) qopts.forceCustomEntityStorage = true\n for (const [k, v] of qpEntries) buildFilter(k, v, isCustomEntity)\n // Server-side full-result search: match the term against the requested fields\n // (defaults to `id`) before pagination so totals/exports stay consistent with\n // the active search instead of filtering only the current client page (#3229).\n if (searchTerm) {\n const fields = searchFields.length ? searchFields : ['id']\n const pattern = `%${searchTerm}%`\n const orClauses = fields.map((field) => ({ [field]: { $ilike: pattern } }))\n ;(filtersObj as any).$or = orClauses\n }\n const res = await qe.query(entityId as any, qopts)\n const rawItems = res.items || []\n const viewPageItems = rawItems.map(mapRow)\n const fullPageItems = rawItems.map(mapFullRow)\n\n // Expose `updated_at` on custom-entity records. The query engine returns only\n // the `doc` fields + `id`, dropping the base `updated_at` column \u2014 which made\n // optimistic locking impossible end-to-end (no version for the edit page to\n // round-trip as the lock header). Batch-read it from storage and merge it in.\n if (isCustomEntity && viewPageItems.length) {\n try {\n const recordIds = viewPageItems\n .map((it: any) => it?.id)\n .filter((v: any): v is string => typeof v === 'string' && v.length > 0)\n if (recordIds.length) {\n const db = em.getKysely()\n const rows = await db\n .selectFrom('custom_entities_storage' as any)\n .select(['entity_id' as any, 'updated_at' as any])\n .where('entity_type' as any, '=', entityId)\n .where('entity_id' as any, 'in', recordIds as any)\n .execute()\n const updatedById = new Map<string, string>()\n for (const row of rows as any[]) {\n const value = row?.updated_at\n const iso = value instanceof Date ? value.toISOString() : (typeof value === 'string' && value.length > 0 ? value : null)\n if (iso && row?.entity_id) updatedById.set(String(row.entity_id), iso)\n }\n for (const item of viewPageItems as any[]) {\n const iso = updatedById.get(String(item?.id))\n if (iso) {\n item.updated_at = iso\n item.updatedAt = iso\n }\n }\n }\n } catch { /* best-effort: locking simply will not engage if storage is unavailable */ }\n }\n\n const total = typeof res.total === 'number' ? res.total : rawItems.length\n const effectivePageSize = res.pageSize || pageSize\n const payload = {\n items: viewPageItems,\n total,\n page: res.page || page,\n pageSize: effectivePageSize,\n totalPages: Math.ceil(total / (effectivePageSize || 1)),\n ...(res.meta?.listCountCapWarning ? { totalIsCapped: true } : {}),\n }\n\n if (requestedExport) {\n const exportItems: any[] = exportFullRequested ? [...fullPageItems] : [...viewPageItems]\n // Short-page termination: `total` is a display value, not a loop bound \u2014 it can\n // under-report (capped counts) or drift while rows are inserted/deleted mid-export.\n // Keep fetching while pages come back full; fail closed at the page ceiling rather\n // than serializing a partial export.\n if (rawItems.length >= pageSize) {\n let nextPage = 2\n for (;;) {\n const nextRes = await qe.query(entityId as any, {\n ...qopts,\n page: { page: nextPage, pageSize },\n })\n const nextRawItems = nextRes.items || []\n if (!nextRawItems.length) break\n const nextViewItems = nextRawItems.map(mapRow)\n const nextFullItems = nextRawItems.map(mapFullRow)\n const nextBatch = exportFullRequested ? nextFullItems : nextViewItems\n exportItems.push(...nextBatch)\n if (nextRawItems.length < pageSize) break\n if (nextPage >= EXPORT_MAX_PAGES) {\n throw new Error(`[internal] export exceeded ${EXPORT_MAX_PAGES} pages; refusing to return a partial export`)\n }\n nextPage += 1\n }\n }\n const prepared = {\n columns: ensureColumns(exportItems),\n rows: exportItems,\n }\n const filenameBase = exportFullRequested ? `${entityId || 'records'}_full` : entityId || 'records'\n const serialized = serializeExport(prepared, requestedExport)\n const filename = defaultExportFilename(filenameBase, requestedExport)\n return new Response(serialized.body, {\n headers: {\n 'content-type': serialized.contentType,\n 'content-disposition': `attachment; filename=\"${filename}\"`,\n },\n })\n }\n\n return NextResponse.json(payload)\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n logger.error('Records GET failed', { err: e })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nconst postBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1).optional(),\n values: z.record(z.string(), z.any()).default({}),\n})\n\nconst putBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1),\n values: z.record(z.string(), z.any()).default({}),\n})\n\nconst mutationResponseSchema = z.object({\n ok: z.literal(true),\n item: z\n .object({\n entityId: z.string(),\n recordId: z.string(),\n })\n .optional(),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let json: unknown\n try { json = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = postBodySchema.safeParse(json)\n if (!parsed.success) return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n const { entityId } = parsed.data\n let { recordId, values } = parsed.data as { recordId?: string; values: Record<string, any> }\n\n try {\n const { resolve } = await createRequestContainer()\n const de = resolve('dataEngine') as any\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n const targetOrgId = resolveSingleOrganizationIdOrDeny(scope, auth)\n if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, isRestricted, rbac })\n // Strip reserved record/system columns the edit form echoes back from the loaded record\n // (`id`, plus `updated_at`/`updatedAt` used for optimistic locking). They are not custom\n // fields; without this they validate as cf_id / cf_updated_at / cf_updatedAt and are\n // rejected as \"Unknown custom field\", which fails EVERY custom-entity edit-form save.\n for (const reservedKey of ['id', 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt']) {\n delete (values as any)[reservedKey]\n }\n const norm = normalizeValues(values)\n\n // Validate against custom field definitions\n try {\n const { validateCustomFieldValuesServer } = await import('../lib/validation')\n const check = await validateCustomFieldValuesServer(em, { entityId, organizationId: targetOrgId, tenantId: auth.tenantId!, values: norm, rejectUndeclaredKeys: true })\n if (!check.ok) return NextResponse.json({ error: 'Validation failed', fields: check.fieldErrors }, { status: 400 })\n } catch { /* ignore if helper missing */ }\n\n const normalizedRecordId = (() => {\n const raw = String(recordId || '').trim()\n if (!raw) return undefined\n const low = raw.toLowerCase()\n if (low === 'create' || low === 'new' || low === 'null' || low === 'undefined') return undefined\n // Enforce UUID only; any non-uuid is ignored so we generate one in the DE\n const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n return uuid.test(raw) ? raw : undefined\n })()\n const { id } = await de.createCustomEntityRecord({\n entityId,\n recordId: normalizedRecordId,\n createOnly: true,\n organizationId: targetOrgId,\n tenantId: auth.tenantId!,\n values: norm,\n })\n\n return NextResponse.json({ ok: true, item: { entityId, recordId: id } })\n } catch (e) {\n if (isCrudHttpError(e)) {\n if (e.body.code === 'custom_entity_record_exists') {\n const { translate } = await resolveTranslations()\n return NextResponse.json({\n ...e.body,\n error: translate('entities.records.errors.alreadyExists', 'A record with this ID already exists. Use PUT to update it.'),\n }, { status: e.status })\n }\n return NextResponse.json(e.body, { status: e.status })\n }\n logger.error('Records POST failed', { err: e })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\n// Avoid zod here to prevent runtime import issues in some environments\nfunction parsePutBody(json: any): { ok: true; data: { entityId: string; recordId: string; values: Record<string, any> } } | { ok: false; error: string } {\n if (!json || typeof json !== 'object') return { ok: false, error: 'Invalid JSON' }\n const entityId = typeof json.entityId === 'string' && json.entityId.length ? json.entityId : ''\n const recordId = typeof json.recordId === 'string' && json.recordId.length ? json.recordId : ''\n const values = (json.values && typeof json.values === 'object') ? json.values as Record<string, any> : {}\n if (!entityId || !recordId) return { ok: false, error: 'entityId and recordId are required' }\n return { ok: true, data: { entityId, recordId, values } }\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let json: any\n try { json = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = parsePutBody(json)\n if (!parsed.ok) return NextResponse.json({ error: parsed.error }, { status: 400 })\n const { entityId, recordId, values } = parsed.data\n\n try {\n const container = await createRequestContainer()\n const { resolve } = container\n const de = resolve('dataEngine') as any\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n const targetOrgId = resolveSingleOrganizationIdOrDeny(scope, auth)\n if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, isRestricted, rbac })\n // Strip reserved record/system columns the edit form echoes back from the loaded record\n // (`id`, plus `updated_at`/`updatedAt` used for optimistic locking). They are not custom\n // fields; without this they validate as cf_id / cf_updated_at / cf_updatedAt and are\n // rejected as \"Unknown custom field\", which fails EVERY custom-entity edit-form save.\n for (const reservedKey of ['id', 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt']) {\n delete (values as any)[reservedKey]\n }\n const norm = normalizeValues(values)\n\n // Validate against custom field definitions\n try {\n const { validateCustomFieldValuesServer } = await import('../lib/validation')\n const check = await validateCustomFieldValuesServer(em, { entityId, organizationId: targetOrgId, tenantId: auth.tenantId!, values: norm, rejectUndeclaredKeys: true })\n if (!check.ok) return NextResponse.json({ error: 'Validation failed', fields: check.fieldErrors }, { status: 400 })\n } catch { /* ignore if helper missing */ }\n\n // Normalize recordId: if blank/sentinel/non-uuid => create instead of update\n const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n const rid = String(recordId || '').trim()\n const low = rid.toLowerCase()\n const isSentinel = !rid || low === 'create' || low === 'new' || low === 'null' || low === 'undefined'\n const isUuid = uuidRe.test(rid)\n if (isSentinel || !isUuid) {\n const created = await de.createCustomEntityRecord({\n entityId,\n recordId: undefined,\n organizationId: targetOrgId,\n tenantId: auth.tenantId!,\n values: norm,\n })\n return NextResponse.json({ ok: true, item: { entityId, recordId: created.id } })\n }\n\n try {\n const currentUpdatedAt = await readCustomEntityRecordUpdatedAt(em, {\n entityType: entityId,\n entityId: rid,\n organizationId: targetOrgId,\n })\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: CUSTOM_ENTITY_RECORD_RESOURCE_KIND,\n resourceId: rid,\n current: currentUpdatedAt,\n request: req,\n })\n } catch (lockError) {\n if (isCrudHttpError(lockError)) {\n return NextResponse.json(lockError.body, { status: lockError.status })\n }\n throw lockError\n }\n\n await de.updateCustomEntityRecord({\n entityId,\n recordId: rid,\n organizationId: targetOrgId,\n tenantId: auth.tenantId!,\n values: norm,\n })\n return NextResponse.json({ ok: true, item: { entityId, recordId: rid } })\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nconst deleteBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1),\n})\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const qpEntityId = url.searchParams.get('entityId')\n const qpRecordId = url.searchParams.get('recordId')\n let payload: any = qpEntityId && qpRecordId ? { entityId: qpEntityId, recordId: qpRecordId } : null\n if (!payload) {\n try { payload = await req.json() } catch { payload = null }\n }\n const parsed = deleteBodySchema.safeParse(payload)\n if (!parsed.success) return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n const { entityId, recordId } = parsed.data\n\n try {\n const container = await createRequestContainer()\n const { resolve } = container\n const de = resolve('dataEngine') as any\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n const targetOrgId = resolveSingleOrganizationIdOrDeny(scope, auth)\n if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, isRestricted, rbac })\n\n try {\n const currentUpdatedAt = await readCustomEntityRecordUpdatedAt(em, {\n entityType: entityId,\n entityId: recordId,\n organizationId: targetOrgId,\n })\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: CUSTOM_ENTITY_RECORD_RESOURCE_KIND,\n resourceId: recordId,\n current: currentUpdatedAt,\n request: req,\n })\n } catch (lockError) {\n if (isCrudHttpError(lockError)) {\n return NextResponse.json(lockError.body, { status: lockError.status })\n }\n throw lockError\n }\n\n await de.deleteCustomEntityRecord({ entityId, recordId, organizationId: targetOrgId, tenantId: auth.tenantId!, soft: true })\n return NextResponse.json({ ok: true })\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nfunction normalizeValues(input: Record<string, any>): Record<string, any> {\n const out: Record<string, any> = {}\n for (const [k, v] of Object.entries(input || {})) {\n const key = k.startsWith('cf_') ? k.replace(/^cf_/, '') : k\n out[key] = v\n }\n return out\n}\n\nconst deleteResponseSchema = z.object({\n ok: z.literal(true),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n}).passthrough()\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'CRUD operations on entity records',\n methods: {\n GET: {\n summary: 'List records',\n description:\n 'Returns paginated records for the supplied entity. Supports custom field filters, exports, and soft-delete toggles.',\n query: listRecordsQuerySchema,\n responses: [\n {\n status: 200,\n description: 'Paginated records',\n schema: listRecordsResponseSchema,\n },\n {\n status: 400,\n description: 'Missing entity id',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n POST: {\n summary: 'Create record',\n description:\n 'Creates a record for the given entity. An existing recordId returns 409; use PUT to update a record. When recordId is omitted or not a UUID the data engine will generate one automatically.',\n requestBody: {\n contentType: 'application/json',\n schema: postBodySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Record created',\n schema: mutationResponseSchema,\n },\n {\n status: 409,\n description: 'Record ID already exists in the current scope',\n schema: errorSchema,\n },\n {\n status: 400,\n description: 'Validation failure',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n PUT: {\n summary: 'Update record',\n description:\n 'Updates an existing record. If the provided recordId is not a UUID the record will be created instead to support optimistic flows.',\n requestBody: {\n contentType: 'application/json',\n schema: putBodySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Record updated',\n schema: mutationResponseSchema,\n },\n {\n status: 400,\n description: 'Validation failure',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n DELETE: {\n summary: 'Delete record',\n description: 'Soft deletes the specified record within the current tenant/org scope.',\n requestBody: {\n contentType: 'application/json',\n schema: deleteBodySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Record deleted',\n schema: deleteResponseSchema,\n },\n {\n status: 400,\n description: 'Missing entity id or record id',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 404,\n description: 'Record not found',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["classifyRecordsEntity"]
7
7
  }
@@ -3,7 +3,11 @@ import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
3
3
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
4
4
  import { resolveCrudRecordId, parseScopedCommandInput } from "@open-mercato/shared/lib/api/scoped";
5
5
  import { createLogger } from "@open-mercato/shared/lib/logger";
6
+ import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
6
7
  import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
8
+ import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
9
+ import { isIdsParamProvided, mergeIdFilter, parseIdsParam } from "@open-mercato/shared/lib/crud/ids";
10
+ import { mergeAdvancedFiltersFromQuery } from "@open-mercato/shared/lib/crud/advanced-filter-integration";
7
11
  import { sanitizeSearchTerm } from "../../helpers.js";
8
12
  import { StaffTimeEntry, StaffTimeEntryTag, StaffTimeProject } from "../../../data/entities.js";
9
13
  import { staffTimeEntryCreateSchema, staffTimeEntryUpdateSchema } from "../../../data/validators.js";
@@ -13,6 +17,7 @@ import { resolveFeatureAccess } from "../../../lib/time-tracking/featureAccess.j
13
17
  import { MANAGE_PROJECTS_FEATURE, resolveProjectAccess } from "../../../lib/time-tracking/access.js";
14
18
  import { readTimeTrackingSettings } from "../../../lib/time-tracking/settings.js";
15
19
  import { decorateTimeEntryRows } from "../../../lib/timesheets/timeEntryDecoration.js";
20
+ import { computeTimeEntryTotals } from "../../../lib/timesheets/timeEntryTotals.js";
16
21
  import { createStaffCrudOpenApi, createPagedListResponseSchema, defaultOkResponseSchema } from "../../openapi.js";
17
22
  const logger = createLogger("staff").child({ component: "api/timesheets/time-entries" });
18
23
  const F = {
@@ -78,7 +83,12 @@ const listSchema = z.object({
78
83
  /** One tag id, or a comma-separated list; an entry matching any of them. */
79
84
  tagIds: z.string().optional(),
80
85
  sortField: z.string().optional(),
81
- sortDir: z.enum(["asc", "desc"]).optional()
86
+ sortDir: z.enum(["asc", "desc"]).optional(),
87
+ /**
88
+ * `true` adds a `totals` object computed over the WHOLE filtered set (not the
89
+ * page). Opt-in so callers that only need rows pay for no aggregate query.
90
+ */
91
+ includeTotals: z.string().optional()
82
92
  }).passthrough();
83
93
  function parseBooleanFlagOrNull(value) {
84
94
  if (typeof value !== "string") return null;
@@ -180,7 +190,7 @@ async function buildScopedTimeEntryListFilters(query, ctx) {
180
190
  filters[F.id] = { $in: entryIds.length > 0 ? entryIds : [IMPOSSIBLE_ID] };
181
191
  }
182
192
  const access = await resolveListProjectAccess(ctx);
183
- if (access.canManageAll) return filters;
193
+ if (access.canManageAll) return mergeAdvancedFiltersFromQuery(filters, query);
184
194
  const branches = [];
185
195
  if (access.projectIds.length > 0) {
186
196
  branches.push({ [F.time_project_id]: { $in: access.projectIds } });
@@ -190,10 +200,10 @@ async function buildScopedTimeEntryListFilters(query, ctx) {
190
200
  }
191
201
  if (branches.length === 0) {
192
202
  filters[F.id] = { $in: [IMPOSSIBLE_ID] };
193
- return filters;
203
+ return mergeAdvancedFiltersFromQuery(filters, query);
194
204
  }
195
205
  filters.$or = branches;
196
- return filters;
206
+ return mergeAdvancedFiltersFromQuery(filters, query);
197
207
  }
198
208
  const timeEntryListFields = [
199
209
  F.id,
@@ -242,6 +252,40 @@ async function decorateTimeEntryList(payload, ctx) {
242
252
  onError: (err) => logger.error("staff.timesheets.time-entries response decoration failed", { err })
243
253
  });
244
254
  }
255
+ function resolveListOrganizationIds(ctx) {
256
+ if (Array.isArray(ctx.organizationIds)) return Array.from(new Set(ctx.organizationIds));
257
+ const fallback = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null;
258
+ return fallback ? [fallback] : [];
259
+ }
260
+ async function attachTimeEntryTotals(payload, ctx) {
261
+ if (parseBooleanToken(ctx.query?.includeTotals ?? null) !== true) return;
262
+ if (!payload || typeof payload !== "object" || !Array.isArray(payload.items)) return;
263
+ delete payload.totals;
264
+ const tenantId = ctx.auth?.tenantId ?? null;
265
+ if (!tenantId) return;
266
+ try {
267
+ const { organizationId } = resolveCtxScope(ctx);
268
+ const query = ctx.query;
269
+ const scopedFilters = await buildScopedTimeEntryListFilters(ctx.query, ctx);
270
+ const filters = mergeIdFilter(scopedFilters, parseIdsParam(query.ids), {
271
+ idsParamProvided: isIdsParamProvided(query.ids)
272
+ });
273
+ const rates = await resolveFeatureAccess(ctx.container, ctx.auth?.sub ?? null, [RATES_FEATURE], {
274
+ tenantId,
275
+ organizationId
276
+ });
277
+ const em = ctx.container.resolve("em");
278
+ payload.totals = await computeTimeEntryTotals(em.getKysely(), filters, {
279
+ tenantId,
280
+ organizationIds: resolveListOrganizationIds(ctx),
281
+ canSeeRates: rates.allowed,
282
+ includeDeleted: parseBooleanToken(typeof query.withDeleted === "string" ? query.withDeleted : null) === true
283
+ });
284
+ } catch (err) {
285
+ logger.error("staff.timesheets.time-entries totals failed", { err });
286
+ getTelemetryRuntime()?.reportError(err, { module: "staff", code: "staff.time_entry_totals_failed" });
287
+ }
288
+ }
245
289
  const crud = makeCrudRoute({
246
290
  metadata: routeMetadata,
247
291
  orm: {
@@ -267,6 +311,9 @@ const crud = makeCrudRoute({
267
311
  },
268
312
  buildFilters: buildScopedTimeEntryListFilters
269
313
  },
314
+ hooks: {
315
+ afterList: attachTimeEntryTotals
316
+ },
270
317
  actions: {
271
318
  create: {
272
319
  commandId: staffTimeEntryCommandIds.create,
@@ -347,11 +394,23 @@ const timeEntryListItemSchema = z.object({
347
394
  /** Present only for a caller holding `staff.timesheets.rates.view`. */
348
395
  currencyCode: z.string().nullable().optional()
349
396
  });
397
+ const timeEntryTotalsSchema = z.object({
398
+ entryCount: z.number(),
399
+ /** Sum of `duration_minutes` across the whole filtered set. */
400
+ durationMinutes: z.number(),
401
+ /** Sum of `rounded_minutes` across the whole filtered set. */
402
+ roundedMinutes: z.number(),
403
+ /** Present only for a caller holding `staff.timesheets.rates.view`; one entry per currency, never added together. */
404
+ money: z.array(z.object({ currencyCode: z.string().nullable(), amount: z.number() })).optional()
405
+ });
350
406
  const openApi = createStaffCrudOpenApi({
351
407
  resourceName: "TimeEntry",
352
408
  pluralName: "TimeEntries",
353
409
  querySchema: listSchema,
354
- listResponseSchema: createPagedListResponseSchema(timeEntryListItemSchema),
410
+ listResponseSchema: createPagedListResponseSchema(timeEntryListItemSchema).extend({
411
+ /** Present only when the request sends `includeTotals=true`; covers every row matching the filters, not just the page. */
412
+ totals: timeEntryTotalsSchema.optional()
413
+ }),
355
414
  create: {
356
415
  schema: staffTimeEntryCreateSchema,
357
416
  description: "Creates a time entry for a staff member. The note is accepted under either `notes` or `description` (`description` wins when both are sent). `roundedMinutes` is derived from the tenant rounding rule at write time and stored; `isBillable` falls back to the project default then the tenant default; `rateCurrencyCode` snapshots the project currency. `tagIds` is applied through the tag assignment command."
@@ -373,6 +432,7 @@ export {
373
432
  POST,
374
433
  PUT,
375
434
  RATES_FEATURE,
435
+ attachTimeEntryTotals,
376
436
  buildScopedTimeEntryListFilters,
377
437
  decorateTimeEntryList,
378
438
  metadata,