@open-mercato/core 0.6.6-develop.6472.1.1673e7e66b → 0.6.6-develop.6480.1.309f47ca1c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/progress/api/active/route.js +1 -1
- package/dist/modules/progress/api/active/route.js.map +2 -2
- package/dist/modules/progress/api/jobs/[id]/route.js +1 -1
- package/dist/modules/progress/api/jobs/[id]/route.js.map +2 -2
- package/dist/modules/progress/api/jobs/route.js +1 -1
- package/dist/modules/progress/api/jobs/route.js.map +2 -2
- package/dist/modules/sales/api/document-history/route.js +10 -0
- package/dist/modules/sales/api/document-history/route.js.map +2 -2
- package/dist/modules/sales/api/notes/route.js +71 -2
- package/dist/modules/sales/api/notes/route.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +12 -8
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/notes.js +15 -7
- package/dist/modules/sales/commands/notes.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/progress/api/active/route.ts +1 -1
- package/src/modules/progress/api/jobs/[id]/route.ts +1 -1
- package/src/modules/progress/api/jobs/route.ts +1 -1
- package/src/modules/sales/api/document-history/route.ts +12 -0
- package/src/modules/sales/api/notes/route.ts +103 -4
- package/src/modules/sales/commands/documents.ts +16 -8
- package/src/modules/sales/commands/notes.ts +15 -7
|
@@ -84,7 +84,13 @@ async function requireContext(em, contextType, contextId, organizationId, tenant
|
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
const repo = contextType === "invoice" ? SalesInvoice : SalesCreditMemo;
|
|
87
|
-
const entity = await
|
|
87
|
+
const entity = await findOneWithDecryption(
|
|
88
|
+
em,
|
|
89
|
+
repo,
|
|
90
|
+
{ id: contextId },
|
|
91
|
+
{},
|
|
92
|
+
{ tenantId, organizationId }
|
|
93
|
+
);
|
|
88
94
|
if (!entity) {
|
|
89
95
|
throw new CrudHttpError(404, { error: "sales.notes.context_not_found" });
|
|
90
96
|
}
|
|
@@ -98,11 +104,11 @@ async function requireContext(em, contextType, contextId, organizationId, tenant
|
|
|
98
104
|
quote: null
|
|
99
105
|
};
|
|
100
106
|
}
|
|
101
|
-
function resolveAuthor(
|
|
102
|
-
|
|
103
|
-
if (!
|
|
107
|
+
function resolveAuthor(authSub) {
|
|
108
|
+
const sub = authSub?.trim();
|
|
109
|
+
if (!sub) return null;
|
|
104
110
|
const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;
|
|
105
|
-
return uuidRegex.test(
|
|
111
|
+
return uuidRegex.test(sub) ? sub : null;
|
|
106
112
|
}
|
|
107
113
|
const createNoteCommand = {
|
|
108
114
|
id: "sales.notes.create",
|
|
@@ -112,7 +118,7 @@ const createNoteCommand = {
|
|
|
112
118
|
ensureOrganizationScope(ctx, parsed.organizationId);
|
|
113
119
|
const em = ctx.container.resolve("em").fork();
|
|
114
120
|
const context = await requireContext(em, parsed.contextType, parsed.contextId, parsed.organizationId, parsed.tenantId);
|
|
115
|
-
const authorUserId = resolveAuthor(
|
|
121
|
+
const authorUserId = resolveAuthor(ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null);
|
|
116
122
|
const note = em.create(SalesNote, {
|
|
117
123
|
organizationId: parsed.organizationId,
|
|
118
124
|
tenantId: parsed.tenantId,
|
|
@@ -230,7 +236,9 @@ const updateNoteCommand = {
|
|
|
230
236
|
ensureTenantScope(ctx, note.tenantId);
|
|
231
237
|
ensureOrganizationScope(ctx, note.organizationId);
|
|
232
238
|
if (parsed.body !== void 0) note.body = parsed.body;
|
|
233
|
-
if (parsed.authorUserId !== void 0)
|
|
239
|
+
if (parsed.authorUserId !== void 0) {
|
|
240
|
+
note.authorUserId = resolveAuthor(ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null);
|
|
241
|
+
}
|
|
234
242
|
if (parsed.appearanceIcon !== void 0) note.appearanceIcon = parsed.appearanceIcon ?? null;
|
|
235
243
|
if (parsed.appearanceColor !== void 0) note.appearanceColor = parsed.appearanceColor ?? null;
|
|
236
244
|
note.updatedAt = /* @__PURE__ */ new Date();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/sales/commands/notes.ts"],
|
|
4
|
-
"sourcesContent": ["import { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'\nimport {\n emitCrudSideEffects,\n emitCrudUndoSideEffects,\n buildChanges,\n requireId,\n} from '@open-mercato/shared/lib/commands/helpers'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'\nimport { E } from '#generated/entities.ids.generated'\nimport {\n SalesNote,\n SalesOrder,\n SalesQuote,\n SalesInvoice,\n SalesCreditMemo,\n type SalesDocumentKind,\n} from '../data/entities'\nimport {\n noteCreateSchema,\n noteUpdateSchema,\n type NoteCreateInput,\n type NoteUpdateInput,\n} from '../data/validators'\nimport { makeCreateRedo } from '@open-mercato/shared/lib/commands/redo'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { ensureOrganizationScope, ensureSameScope, ensureTenantScope, extractUndoPayload } from './shared'\n\ntype NoteSnapshot = {\n id: string\n organizationId: string\n tenantId: string\n contextType: SalesDocumentKind\n contextId: string\n orderId: string | null\n quoteId: string | null\n body: string\n authorUserId: string | null\n appearanceIcon: string | null\n appearanceColor: string | null\n}\n\ntype NoteUndoPayload = {\n before?: NoteSnapshot | null\n after?: NoteSnapshot | null\n}\n\nconst noteCrudIndexer = {\n entityType: E.sales.sales_note,\n}\n\nconst noteCrudEvents: CrudEventsConfig = {\n module: 'sales',\n entity: 'note',\n persistent: true,\n buildPayload: (ctx) => ({\n id: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n }),\n}\n\nasync function loadNoteSnapshot(em: EntityManager, id: string): Promise<NoteSnapshot | null> {\n const note = await findOneWithDecryption(em, SalesNote, { id }, {})\n if (!note) return null\n return {\n id: note.id,\n organizationId: note.organizationId,\n tenantId: note.tenantId,\n contextType: note.contextType,\n contextId: note.contextId,\n orderId: note.order ? (typeof note.order === 'string' ? note.order : note.order.id) : null,\n quoteId: note.quote ? (typeof note.quote === 'string' ? note.quote : note.quote.id) : null,\n body: note.body,\n authorUserId: note.authorUserId ?? null,\n appearanceIcon: note.appearanceIcon ?? null,\n appearanceColor: note.appearanceColor ?? null,\n }\n}\n\nasync function requireContext(\n em: EntityManager,\n contextType: SalesDocumentKind,\n contextId: string,\n organizationId?: string,\n tenantId?: string\n): Promise<{\n organizationId: string\n tenantId: string\n order?: SalesOrder | null\n quote?: SalesQuote | null\n}> {\n if (contextType === 'order') {\n const order = await findOneWithDecryption(em, SalesOrder, { id: contextId }, {}, { tenantId, organizationId })\n if (!order) {\n throw new CrudHttpError(404, { error: 'sales.notes.context_not_found' })\n }\n if (organizationId && tenantId) {\n ensureSameScope(order, organizationId, tenantId)\n }\n return {\n organizationId: order.organizationId,\n tenantId: order.tenantId,\n order,\n quote: null,\n }\n }\n if (contextType === 'quote') {\n const quote = await findOneWithDecryption(em, SalesQuote, { id: contextId }, {}, { tenantId, organizationId })\n if (!quote) {\n throw new CrudHttpError(404, { error: 'sales.notes.context_not_found' })\n }\n if (organizationId && tenantId) {\n ensureSameScope(quote, organizationId, tenantId)\n }\n return {\n organizationId: quote.organizationId,\n tenantId: quote.tenantId,\n order: null,\n quote,\n }\n }\n const repo = contextType === 'invoice' ? SalesInvoice : SalesCreditMemo\n const entity = await em.findOne(repo as any, { id: contextId }) as (SalesInvoice | SalesCreditMemo) | null\n if (!entity) {\n throw new CrudHttpError(404, { error: 'sales.notes.context_not_found' })\n }\n if (organizationId && tenantId) {\n ensureSameScope(entity, organizationId, tenantId)\n }\n return {\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n order: null,\n quote: null,\n }\n}\n\nfunction resolveAuthor(inputAuthor: string | undefined, authSub: string | null): string | null {\n if (inputAuthor) return inputAuthor\n if (!authSub) return null\n const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/\n return uuidRegex.test(authSub) ? authSub : null\n}\n\nconst createNoteCommand: CommandHandler<NoteCreateInput, { noteId: string; authorUserId: string | null }> = {\n id: 'sales.notes.create',\n async execute(rawInput, ctx) {\n const parsed = noteCreateSchema.parse(rawInput ?? {})\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const context = await requireContext(em, parsed.contextType, parsed.contextId, parsed.organizationId, parsed.tenantId)\n const authorUserId = resolveAuthor(parsed.authorUserId, ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null)\n\n const note = em.create(SalesNote, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n contextType: parsed.contextType,\n contextId: parsed.contextId,\n order: context.order ?? null,\n quote: context.quote ?? null,\n authorUserId,\n appearanceIcon: parsed.appearanceIcon ?? null,\n appearanceColor: parsed.appearanceColor ?? null,\n body: parsed.body,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(note)\n await em.flush()\n\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine,\n action: 'created',\n entity: note,\n identifiers: {\n id: note.id,\n organizationId: note.organizationId,\n tenantId: note.tenantId,\n },\n indexer: noteCrudIndexer,\n events: noteCrudEvents,\n })\n\n return { noteId: note.id, authorUserId: note.authorUserId ?? null }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return loadNoteSnapshot(em, result.noteId)\n },\n buildLog: async ({ result, snapshots }) => {\n const { translate } = await resolveTranslations()\n const snapshot = snapshots.after as NoteSnapshot | undefined\n return {\n actionLabel: translate('sales.audit.notes.create', 'Create note'),\n resourceKind: 'sales.note',\n resourceId: result.noteId,\n parentResourceKind: snapshot?.contextType ? `sales.${snapshot.contextType}` : null,\n parentResourceId: snapshot?.contextId ?? null,\n tenantId: snapshot?.tenantId ?? null,\n organizationId: snapshot?.organizationId ?? null,\n snapshotAfter: snapshot ?? null,\n payload: {\n undo: {\n after: snapshot ?? null,\n } satisfies NoteUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const noteId = logEntry?.resourceId ?? null\n if (!noteId) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const existing = await findOneWithDecryption(em, SalesNote, { id: noteId }, {})\n if (existing) {\n em.remove(existing)\n await em.flush()\n }\n },\n redo: makeCreateRedo<SalesNote, NoteSnapshot, NoteCreateInput, { noteId: string; authorUserId: string | null }>({\n entityClass: SalesNote,\n indexer: noteCrudIndexer,\n events: noteCrudEvents,\n findRow: ({ em, id, snapshot }) =>\n findOneWithDecryption(\n em,\n SalesNote,\n { id },\n undefined,\n { tenantId: snapshot.tenantId, organizationId: snapshot.organizationId },\n ),\n seedFromSnapshot: (snapshot) => ({\n id: snapshot.id,\n organizationId: snapshot.organizationId,\n tenantId: snapshot.tenantId,\n contextType: snapshot.contextType,\n contextId: snapshot.contextId,\n body: snapshot.body,\n authorUserId: snapshot.authorUserId,\n appearanceIcon: snapshot.appearanceIcon,\n appearanceColor: snapshot.appearanceColor,\n createdAt: new Date(),\n updatedAt: new Date(),\n }),\n beforeRestore: async ({ em, snapshot }) => {\n const context = await requireContext(em, snapshot.contextType, snapshot.contextId).catch(() => null)\n if (!context) {\n throw new CrudHttpError(404, { error: 'sales.notes.context_not_found' })\n }\n return {\n order: snapshot.orderId ? context.order ?? null : null,\n quote: snapshot.quoteId ? context.quote ?? null : null,\n }\n },\n buildResult: (entity) => ({ noteId: entity.id, authorUserId: entity.authorUserId ?? null }),\n }),\n}\n\nconst updateNoteCommand: CommandHandler<NoteUpdateInput, { noteId: string }> = {\n id: 'sales.notes.update',\n async prepare(rawInput, ctx) {\n const parsed = noteUpdateSchema.parse(rawInput ?? {})\n const em = ctx.container.resolve('em') as EntityManager\n const snapshot = await loadNoteSnapshot(em, parsed.id)\n return snapshot ? { before: snapshot } : {}\n },\n async execute(rawInput, ctx) {\n const parsed = noteUpdateSchema.parse(rawInput ?? {})\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const note = await findOneWithDecryption(em, SalesNote, { id: parsed.id }, {})\n if (!note) throw new CrudHttpError(404, { error: 'sales.notes.not_found' })\n ensureTenantScope(ctx, note.tenantId)\n ensureOrganizationScope(ctx, note.organizationId)\n\n if (parsed.body !== undefined) note.body = parsed.body\n if (parsed.authorUserId !== undefined) note.authorUserId = parsed.authorUserId ?? null\n if (parsed.appearanceIcon !== undefined) note.appearanceIcon = parsed.appearanceIcon ?? null\n if (parsed.appearanceColor !== undefined) note.appearanceColor = parsed.appearanceColor ?? null\n note.updatedAt = new Date()\n\n await em.flush()\n\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: note,\n identifiers: {\n id: note.id,\n organizationId: note.organizationId,\n tenantId: note.tenantId,\n },\n indexer: noteCrudIndexer,\n events: noteCrudEvents,\n })\n\n return { noteId: note.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return await loadNoteSnapshot(em, result.noteId)\n },\n buildLog: async ({ snapshots }) => {\n const { translate } = await resolveTranslations()\n const before = snapshots.before as NoteSnapshot | undefined\n if (!before) return null\n const after = snapshots.after as NoteSnapshot | undefined\n const changes =\n after && before\n ? buildChanges(\n before as unknown as Record<string, unknown>,\n after as unknown as Record<string, unknown>,\n ['body', 'authorUserId', 'appearanceIcon', 'appearanceColor']\n )\n : {}\n return {\n actionLabel: translate('sales.audit.notes.update', 'Update note'),\n resourceKind: 'sales.note',\n resourceId: before.id,\n parentResourceKind: before.contextType ? `sales.${before.contextType}` : null,\n parentResourceId: before.contextId ?? null,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n snapshotAfter: after ?? null,\n changes,\n payload: {\n undo: {\n before,\n after: after ?? null,\n } satisfies NoteUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<NoteUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const context = await requireContext(em, before.contextType, before.contextId).catch(() => null)\n if (!context) return\n let note = await findOneWithDecryption(em, SalesNote, { id: before.id }, {}, { tenantId: before.tenantId, organizationId: before.organizationId })\n if (!note) {\n note = em.create(SalesNote, {\n id: before.id,\n organizationId: before.organizationId,\n tenantId: before.tenantId,\n contextType: before.contextType,\n contextId: before.contextId,\n order: before.orderId ? context.order ?? null : null,\n quote: before.quoteId ? context.quote ?? null : null,\n body: before.body,\n authorUserId: before.authorUserId,\n appearanceIcon: before.appearanceIcon,\n appearanceColor: before.appearanceColor,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(note)\n }\n note.organizationId = before.organizationId\n note.tenantId = before.tenantId\n note.contextType = before.contextType\n note.contextId = before.contextId\n note.order = before.orderId ? context.order ?? null : null\n note.quote = before.quoteId ? context.quote ?? null : null\n note.body = before.body\n note.authorUserId = before.authorUserId\n note.appearanceIcon = before.appearanceIcon\n note.appearanceColor = before.appearanceColor\n note.updatedAt = new Date()\n await em.flush()\n\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudUndoSideEffects({\n dataEngine,\n action: 'updated',\n entity: note,\n identifiers: {\n id: note.id,\n organizationId: note.organizationId,\n tenantId: note.tenantId,\n },\n indexer: noteCrudIndexer,\n events: noteCrudEvents,\n })\n },\n}\n\nconst deleteNoteCommand: CommandHandler<{ body?: Record<string, unknown>; query?: Record<string, unknown> }, { noteId: string }> =\n {\n id: 'sales.notes.delete',\n async prepare(input, ctx) {\n const id = requireId(input, 'Note id required')\n const em = ctx.container.resolve('em') as EntityManager\n const snapshot = await loadNoteSnapshot(em, id)\n return snapshot ? { before: snapshot } : {}\n },\n async execute(input, ctx) {\n const id = requireId(input, 'Note id required')\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const note = await findOneWithDecryption(em, SalesNote, { id }, {})\n if (!note) throw new CrudHttpError(404, { error: 'sales.notes.not_found' })\n ensureTenantScope(ctx, note.tenantId)\n ensureOrganizationScope(ctx, note.organizationId)\n em.remove(note)\n await em.flush()\n\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine,\n action: 'deleted',\n entity: note,\n identifiers: {\n id: note.id,\n organizationId: note.organizationId,\n tenantId: note.tenantId,\n },\n indexer: noteCrudIndexer,\n events: noteCrudEvents,\n })\n return { noteId: note.id }\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as NoteSnapshot | undefined\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('sales.audit.notes.delete', 'Delete note'),\n resourceKind: 'sales.note',\n resourceId: before.id,\n parentResourceKind: before.contextType ? `sales.${before.contextType}` : null,\n parentResourceId: before.contextId ?? null,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n payload: {\n undo: {\n before,\n } satisfies NoteUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<NoteUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const context = await requireContext(em, before.contextType, before.contextId).catch(() => null)\n if (!context) return\n let note = await findOneWithDecryption(em, SalesNote, { id: before.id }, {}, { tenantId: before.tenantId, organizationId: before.organizationId })\n if (!note) {\n note = em.create(SalesNote, {\n id: before.id,\n organizationId: before.organizationId,\n tenantId: before.tenantId,\n contextType: before.contextType,\n contextId: before.contextId,\n order: before.orderId ? context.order ?? null : null,\n quote: before.quoteId ? context.quote ?? null : null,\n body: before.body,\n authorUserId: before.authorUserId,\n appearanceIcon: before.appearanceIcon,\n appearanceColor: before.appearanceColor,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(note)\n }\n note.organizationId = before.organizationId\n note.tenantId = before.tenantId\n note.contextType = before.contextType\n note.contextId = before.contextId\n note.order = before.orderId ? context.order ?? null : null\n note.quote = before.quoteId ? context.quote ?? null : null\n note.body = before.body\n note.authorUserId = before.authorUserId\n note.appearanceIcon = before.appearanceIcon\n note.appearanceColor = before.appearanceColor\n note.updatedAt = new Date()\n await em.flush()\n\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudUndoSideEffects({\n dataEngine,\n action: 'created',\n entity: note,\n identifiers: {\n id: note.id,\n organizationId: note.organizationId,\n tenantId: note.tenantId,\n },\n indexer: noteCrudIndexer,\n events: noteCrudEvents,\n })\n },\n }\n\nregisterCommand(createNoteCommand)\nregisterCommand(updateNoteCommand)\nregisterCommand(deleteNoteCommand)\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'\nimport {\n emitCrudSideEffects,\n emitCrudUndoSideEffects,\n buildChanges,\n requireId,\n} from '@open-mercato/shared/lib/commands/helpers'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'\nimport { E } from '#generated/entities.ids.generated'\nimport {\n SalesNote,\n SalesOrder,\n SalesQuote,\n SalesInvoice,\n SalesCreditMemo,\n type SalesDocumentKind,\n} from '../data/entities'\nimport {\n noteCreateSchema,\n noteUpdateSchema,\n type NoteCreateInput,\n type NoteUpdateInput,\n} from '../data/validators'\nimport { makeCreateRedo } from '@open-mercato/shared/lib/commands/redo'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { ensureOrganizationScope, ensureSameScope, ensureTenantScope, extractUndoPayload } from './shared'\n\ntype NoteSnapshot = {\n id: string\n organizationId: string\n tenantId: string\n contextType: SalesDocumentKind\n contextId: string\n orderId: string | null\n quoteId: string | null\n body: string\n authorUserId: string | null\n appearanceIcon: string | null\n appearanceColor: string | null\n}\n\ntype NoteUndoPayload = {\n before?: NoteSnapshot | null\n after?: NoteSnapshot | null\n}\n\nconst noteCrudIndexer = {\n entityType: E.sales.sales_note,\n}\n\nconst noteCrudEvents: CrudEventsConfig = {\n module: 'sales',\n entity: 'note',\n persistent: true,\n buildPayload: (ctx) => ({\n id: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n }),\n}\n\nasync function loadNoteSnapshot(em: EntityManager, id: string): Promise<NoteSnapshot | null> {\n const note = await findOneWithDecryption(em, SalesNote, { id }, {})\n if (!note) return null\n return {\n id: note.id,\n organizationId: note.organizationId,\n tenantId: note.tenantId,\n contextType: note.contextType,\n contextId: note.contextId,\n orderId: note.order ? (typeof note.order === 'string' ? note.order : note.order.id) : null,\n quoteId: note.quote ? (typeof note.quote === 'string' ? note.quote : note.quote.id) : null,\n body: note.body,\n authorUserId: note.authorUserId ?? null,\n appearanceIcon: note.appearanceIcon ?? null,\n appearanceColor: note.appearanceColor ?? null,\n }\n}\n\nasync function requireContext(\n em: EntityManager,\n contextType: SalesDocumentKind,\n contextId: string,\n organizationId?: string,\n tenantId?: string\n): Promise<{\n organizationId: string\n tenantId: string\n order?: SalesOrder | null\n quote?: SalesQuote | null\n}> {\n if (contextType === 'order') {\n const order = await findOneWithDecryption(em, SalesOrder, { id: contextId }, {}, { tenantId, organizationId })\n if (!order) {\n throw new CrudHttpError(404, { error: 'sales.notes.context_not_found' })\n }\n if (organizationId && tenantId) {\n ensureSameScope(order, organizationId, tenantId)\n }\n return {\n organizationId: order.organizationId,\n tenantId: order.tenantId,\n order,\n quote: null,\n }\n }\n if (contextType === 'quote') {\n const quote = await findOneWithDecryption(em, SalesQuote, { id: contextId }, {}, { tenantId, organizationId })\n if (!quote) {\n throw new CrudHttpError(404, { error: 'sales.notes.context_not_found' })\n }\n if (organizationId && tenantId) {\n ensureSameScope(quote, organizationId, tenantId)\n }\n return {\n organizationId: quote.organizationId,\n tenantId: quote.tenantId,\n order: null,\n quote,\n }\n }\n const repo = contextType === 'invoice' ? SalesInvoice : SalesCreditMemo\n const entity = await findOneWithDecryption(\n em,\n repo as any,\n { id: contextId },\n {},\n { tenantId, organizationId },\n ) as (SalesInvoice | SalesCreditMemo) | null\n if (!entity) {\n throw new CrudHttpError(404, { error: 'sales.notes.context_not_found' })\n }\n if (organizationId && tenantId) {\n ensureSameScope(entity, organizationId, tenantId)\n }\n return {\n organizationId: entity.organizationId,\n tenantId: entity.tenantId,\n order: null,\n quote: null,\n }\n}\n\nfunction resolveAuthor(authSub: string | null): string | null {\n const sub = authSub?.trim()\n if (!sub) return null\n const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/\n return uuidRegex.test(sub) ? sub : null\n}\n\nconst createNoteCommand: CommandHandler<NoteCreateInput, { noteId: string; authorUserId: string | null }> = {\n id: 'sales.notes.create',\n async execute(rawInput, ctx) {\n const parsed = noteCreateSchema.parse(rawInput ?? {})\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const context = await requireContext(em, parsed.contextType, parsed.contextId, parsed.organizationId, parsed.tenantId)\n const authorUserId = resolveAuthor(ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null)\n\n const note = em.create(SalesNote, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n contextType: parsed.contextType,\n contextId: parsed.contextId,\n order: context.order ?? null,\n quote: context.quote ?? null,\n authorUserId,\n appearanceIcon: parsed.appearanceIcon ?? null,\n appearanceColor: parsed.appearanceColor ?? null,\n body: parsed.body,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(note)\n await em.flush()\n\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine,\n action: 'created',\n entity: note,\n identifiers: {\n id: note.id,\n organizationId: note.organizationId,\n tenantId: note.tenantId,\n },\n indexer: noteCrudIndexer,\n events: noteCrudEvents,\n })\n\n return { noteId: note.id, authorUserId: note.authorUserId ?? null }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return loadNoteSnapshot(em, result.noteId)\n },\n buildLog: async ({ result, snapshots }) => {\n const { translate } = await resolveTranslations()\n const snapshot = snapshots.after as NoteSnapshot | undefined\n return {\n actionLabel: translate('sales.audit.notes.create', 'Create note'),\n resourceKind: 'sales.note',\n resourceId: result.noteId,\n parentResourceKind: snapshot?.contextType ? `sales.${snapshot.contextType}` : null,\n parentResourceId: snapshot?.contextId ?? null,\n tenantId: snapshot?.tenantId ?? null,\n organizationId: snapshot?.organizationId ?? null,\n snapshotAfter: snapshot ?? null,\n payload: {\n undo: {\n after: snapshot ?? null,\n } satisfies NoteUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const noteId = logEntry?.resourceId ?? null\n if (!noteId) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const existing = await findOneWithDecryption(em, SalesNote, { id: noteId }, {})\n if (existing) {\n em.remove(existing)\n await em.flush()\n }\n },\n redo: makeCreateRedo<SalesNote, NoteSnapshot, NoteCreateInput, { noteId: string; authorUserId: string | null }>({\n entityClass: SalesNote,\n indexer: noteCrudIndexer,\n events: noteCrudEvents,\n findRow: ({ em, id, snapshot }) =>\n findOneWithDecryption(\n em,\n SalesNote,\n { id },\n undefined,\n { tenantId: snapshot.tenantId, organizationId: snapshot.organizationId },\n ),\n seedFromSnapshot: (snapshot) => ({\n id: snapshot.id,\n organizationId: snapshot.organizationId,\n tenantId: snapshot.tenantId,\n contextType: snapshot.contextType,\n contextId: snapshot.contextId,\n body: snapshot.body,\n authorUserId: snapshot.authorUserId,\n appearanceIcon: snapshot.appearanceIcon,\n appearanceColor: snapshot.appearanceColor,\n createdAt: new Date(),\n updatedAt: new Date(),\n }),\n beforeRestore: async ({ em, snapshot }) => {\n const context = await requireContext(em, snapshot.contextType, snapshot.contextId).catch(() => null)\n if (!context) {\n throw new CrudHttpError(404, { error: 'sales.notes.context_not_found' })\n }\n return {\n order: snapshot.orderId ? context.order ?? null : null,\n quote: snapshot.quoteId ? context.quote ?? null : null,\n }\n },\n buildResult: (entity) => ({ noteId: entity.id, authorUserId: entity.authorUserId ?? null }),\n }),\n}\n\nconst updateNoteCommand: CommandHandler<NoteUpdateInput, { noteId: string }> = {\n id: 'sales.notes.update',\n async prepare(rawInput, ctx) {\n const parsed = noteUpdateSchema.parse(rawInput ?? {})\n const em = ctx.container.resolve('em') as EntityManager\n const snapshot = await loadNoteSnapshot(em, parsed.id)\n return snapshot ? { before: snapshot } : {}\n },\n async execute(rawInput, ctx) {\n const parsed = noteUpdateSchema.parse(rawInput ?? {})\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const note = await findOneWithDecryption(em, SalesNote, { id: parsed.id }, {})\n if (!note) throw new CrudHttpError(404, { error: 'sales.notes.not_found' })\n ensureTenantScope(ctx, note.tenantId)\n ensureOrganizationScope(ctx, note.organizationId)\n\n if (parsed.body !== undefined) note.body = parsed.body\n if (parsed.authorUserId !== undefined) {\n note.authorUserId = resolveAuthor(ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null)\n }\n if (parsed.appearanceIcon !== undefined) note.appearanceIcon = parsed.appearanceIcon ?? null\n if (parsed.appearanceColor !== undefined) note.appearanceColor = parsed.appearanceColor ?? null\n note.updatedAt = new Date()\n\n await em.flush()\n\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: note,\n identifiers: {\n id: note.id,\n organizationId: note.organizationId,\n tenantId: note.tenantId,\n },\n indexer: noteCrudIndexer,\n events: noteCrudEvents,\n })\n\n return { noteId: note.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return await loadNoteSnapshot(em, result.noteId)\n },\n buildLog: async ({ snapshots }) => {\n const { translate } = await resolveTranslations()\n const before = snapshots.before as NoteSnapshot | undefined\n if (!before) return null\n const after = snapshots.after as NoteSnapshot | undefined\n const changes =\n after && before\n ? buildChanges(\n before as unknown as Record<string, unknown>,\n after as unknown as Record<string, unknown>,\n ['body', 'authorUserId', 'appearanceIcon', 'appearanceColor']\n )\n : {}\n return {\n actionLabel: translate('sales.audit.notes.update', 'Update note'),\n resourceKind: 'sales.note',\n resourceId: before.id,\n parentResourceKind: before.contextType ? `sales.${before.contextType}` : null,\n parentResourceId: before.contextId ?? null,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n snapshotAfter: after ?? null,\n changes,\n payload: {\n undo: {\n before,\n after: after ?? null,\n } satisfies NoteUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<NoteUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const context = await requireContext(em, before.contextType, before.contextId).catch(() => null)\n if (!context) return\n let note = await findOneWithDecryption(em, SalesNote, { id: before.id }, {}, { tenantId: before.tenantId, organizationId: before.organizationId })\n if (!note) {\n note = em.create(SalesNote, {\n id: before.id,\n organizationId: before.organizationId,\n tenantId: before.tenantId,\n contextType: before.contextType,\n contextId: before.contextId,\n order: before.orderId ? context.order ?? null : null,\n quote: before.quoteId ? context.quote ?? null : null,\n body: before.body,\n authorUserId: before.authorUserId,\n appearanceIcon: before.appearanceIcon,\n appearanceColor: before.appearanceColor,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(note)\n }\n note.organizationId = before.organizationId\n note.tenantId = before.tenantId\n note.contextType = before.contextType\n note.contextId = before.contextId\n note.order = before.orderId ? context.order ?? null : null\n note.quote = before.quoteId ? context.quote ?? null : null\n note.body = before.body\n note.authorUserId = before.authorUserId\n note.appearanceIcon = before.appearanceIcon\n note.appearanceColor = before.appearanceColor\n note.updatedAt = new Date()\n await em.flush()\n\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudUndoSideEffects({\n dataEngine,\n action: 'updated',\n entity: note,\n identifiers: {\n id: note.id,\n organizationId: note.organizationId,\n tenantId: note.tenantId,\n },\n indexer: noteCrudIndexer,\n events: noteCrudEvents,\n })\n },\n}\n\nconst deleteNoteCommand: CommandHandler<{ body?: Record<string, unknown>; query?: Record<string, unknown> }, { noteId: string }> =\n {\n id: 'sales.notes.delete',\n async prepare(input, ctx) {\n const id = requireId(input, 'Note id required')\n const em = ctx.container.resolve('em') as EntityManager\n const snapshot = await loadNoteSnapshot(em, id)\n return snapshot ? { before: snapshot } : {}\n },\n async execute(input, ctx) {\n const id = requireId(input, 'Note id required')\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const note = await findOneWithDecryption(em, SalesNote, { id }, {})\n if (!note) throw new CrudHttpError(404, { error: 'sales.notes.not_found' })\n ensureTenantScope(ctx, note.tenantId)\n ensureOrganizationScope(ctx, note.organizationId)\n em.remove(note)\n await em.flush()\n\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine,\n action: 'deleted',\n entity: note,\n identifiers: {\n id: note.id,\n organizationId: note.organizationId,\n tenantId: note.tenantId,\n },\n indexer: noteCrudIndexer,\n events: noteCrudEvents,\n })\n return { noteId: note.id }\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as NoteSnapshot | undefined\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('sales.audit.notes.delete', 'Delete note'),\n resourceKind: 'sales.note',\n resourceId: before.id,\n parentResourceKind: before.contextType ? `sales.${before.contextType}` : null,\n parentResourceId: before.contextId ?? null,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n payload: {\n undo: {\n before,\n } satisfies NoteUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<NoteUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const context = await requireContext(em, before.contextType, before.contextId).catch(() => null)\n if (!context) return\n let note = await findOneWithDecryption(em, SalesNote, { id: before.id }, {}, { tenantId: before.tenantId, organizationId: before.organizationId })\n if (!note) {\n note = em.create(SalesNote, {\n id: before.id,\n organizationId: before.organizationId,\n tenantId: before.tenantId,\n contextType: before.contextType,\n contextId: before.contextId,\n order: before.orderId ? context.order ?? null : null,\n quote: before.quoteId ? context.quote ?? null : null,\n body: before.body,\n authorUserId: before.authorUserId,\n appearanceIcon: before.appearanceIcon,\n appearanceColor: before.appearanceColor,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(note)\n }\n note.organizationId = before.organizationId\n note.tenantId = before.tenantId\n note.contextType = before.contextType\n note.contextId = before.contextId\n note.order = before.orderId ? context.order ?? null : null\n note.quote = before.quoteId ? context.quote ?? null : null\n note.body = before.body\n note.authorUserId = before.authorUserId\n note.appearanceIcon = before.appearanceIcon\n note.appearanceColor = before.appearanceColor\n note.updatedAt = new Date()\n await em.flush()\n\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudUndoSideEffects({\n dataEngine,\n action: 'created',\n entity: note,\n identifiers: {\n id: note.id,\n organizationId: note.organizationId,\n tenantId: note.tenantId,\n },\n indexer: noteCrudIndexer,\n events: noteCrudEvents,\n })\n },\n }\n\nregisterCommand(createNoteCommand)\nregisterCommand(updateNoteCommand)\nregisterCommand(deleteNoteCommand)\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6480.1.309f47ca1c",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -253,16 +253,16 @@
|
|
|
253
253
|
"zod": "^4.4.3"
|
|
254
254
|
},
|
|
255
255
|
"peerDependencies": {
|
|
256
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
256
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6480.1.309f47ca1c",
|
|
257
|
+
"@open-mercato/shared": "0.6.6-develop.6480.1.309f47ca1c",
|
|
258
|
+
"@open-mercato/ui": "0.6.6-develop.6480.1.309f47ca1c",
|
|
259
259
|
"react": "^19.0.0",
|
|
260
260
|
"react-dom": "^19.0.0"
|
|
261
261
|
},
|
|
262
262
|
"devDependencies": {
|
|
263
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
264
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
265
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
263
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6480.1.309f47ca1c",
|
|
264
|
+
"@open-mercato/shared": "0.6.6-develop.6480.1.309f47ca1c",
|
|
265
|
+
"@open-mercato/ui": "0.6.6-develop.6480.1.309f47ca1c",
|
|
266
266
|
"@testing-library/dom": "^10.4.1",
|
|
267
267
|
"@testing-library/jest-dom": "^6.9.1",
|
|
268
268
|
"@testing-library/react": "^16.3.1",
|
|
@@ -4,7 +4,7 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
|
4
4
|
import type { ProgressJob } from '../../data/entities'
|
|
5
5
|
|
|
6
6
|
const routeMetadata = {
|
|
7
|
-
GET: { requireAuth: true },
|
|
7
|
+
GET: { requireAuth: true, requireFeatures: ['progress.view'] },
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export const metadata = routeMetadata
|
|
@@ -7,7 +7,7 @@ import { updateProgressSchema } from '../../../data/validators'
|
|
|
7
7
|
import type { ProgressService } from '../../../lib/progressService'
|
|
8
8
|
|
|
9
9
|
const routeMetadata = {
|
|
10
|
-
GET: { requireAuth: true },
|
|
10
|
+
GET: { requireAuth: true, requireFeatures: ['progress.view'] },
|
|
11
11
|
PUT: { requireAuth: true, requireFeatures: ['progress.update'] },
|
|
12
12
|
DELETE: { requireAuth: true, requireFeatures: ['progress.cancel'] },
|
|
13
13
|
}
|
|
@@ -16,7 +16,7 @@ import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
|
16
16
|
const logger = createLogger('progress').child({ component: 'jobs' })
|
|
17
17
|
|
|
18
18
|
const routeMetadata = {
|
|
19
|
-
GET: { requireAuth: true },
|
|
19
|
+
GET: { requireAuth: true, requireFeatures: ['progress.view'] },
|
|
20
20
|
POST: { requireAuth: true, requireFeatures: ['progress.create'] },
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -9,6 +9,7 @@ import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/er
|
|
|
9
9
|
import { NextResponse } from 'next/server'
|
|
10
10
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
11
11
|
import type { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'
|
|
12
|
+
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
12
13
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
13
14
|
import { buildHistoryEntries } from '../../lib/historyHelpers'
|
|
14
15
|
import { SalesNote } from '../../data/entities'
|
|
@@ -73,6 +74,16 @@ export async function GET(req: Request) {
|
|
|
73
74
|
})
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
const requiredFeature = query.kind === 'order' ? 'sales.orders.view' : 'sales.quotes.view'
|
|
78
|
+
const rbac = container.resolve('rbacService') as RbacService
|
|
79
|
+
const hasAccess = await rbac.userHasAllFeatures(auth.sub, [requiredFeature], {
|
|
80
|
+
tenantId: auth.tenantId,
|
|
81
|
+
organizationId,
|
|
82
|
+
})
|
|
83
|
+
if (!hasAccess) {
|
|
84
|
+
throw new CrudHttpError(403, { error: translate('api.errors.forbidden', 'Forbidden') })
|
|
85
|
+
}
|
|
86
|
+
|
|
76
87
|
const resourceKind = query.kind === 'order' ? 'sales.order' : 'sales.quote'
|
|
77
88
|
|
|
78
89
|
const actionLogService = container.resolve('actionLogService') as ActionLogService
|
|
@@ -178,6 +189,7 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
178
189
|
{ status: 200, description: 'History entries', schema: documentHistoryResponseSchema },
|
|
179
190
|
{ status: 400, description: 'Invalid query', schema: z.object({ error: z.string() }) },
|
|
180
191
|
{ status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },
|
|
192
|
+
{ status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },
|
|
181
193
|
{ status: 404, description: 'Document not found', schema: z.object({ error: z.string() }) },
|
|
182
194
|
],
|
|
183
195
|
},
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
|
+
import { makeCrudRoute, type CrudCtx } from '@open-mercato/shared/lib/crud/factory'
|
|
3
4
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
5
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
6
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
5
|
-
import {
|
|
7
|
+
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
8
|
+
import { SalesNote, type SalesDocumentKind } from '../../data/entities'
|
|
6
9
|
import { noteCreateSchema, noteUpdateSchema } from '../../data/validators'
|
|
7
10
|
import { withScopedPayload } from '../utils'
|
|
8
11
|
import { E } from '#generated/entities.ids.generated'
|
|
@@ -27,6 +30,91 @@ const listSchema = z
|
|
|
27
30
|
})
|
|
28
31
|
.passthrough()
|
|
29
32
|
|
|
33
|
+
type NoteListQuery = z.infer<typeof listSchema>
|
|
34
|
+
type NoteAction = 'view' | 'manage'
|
|
35
|
+
|
|
36
|
+
const noteFeatureByContext: Record<SalesDocumentKind, Record<NoteAction, string>> = {
|
|
37
|
+
order: { view: 'sales.orders.view', manage: 'sales.orders.manage' },
|
|
38
|
+
quote: { view: 'sales.quotes.view', manage: 'sales.quotes.manage' },
|
|
39
|
+
invoice: { view: 'sales.invoices.manage', manage: 'sales.invoices.manage' },
|
|
40
|
+
credit_memo: { view: 'sales.credit_memos.manage', manage: 'sales.credit_memos.manage' },
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const allNoteViewFeatures = Array.from(
|
|
44
|
+
new Set(Object.values(noteFeatureByContext).map((features) => features.view)),
|
|
45
|
+
)
|
|
46
|
+
const allNoteManageFeatures = Array.from(
|
|
47
|
+
new Set(Object.values(noteFeatureByContext).map((features) => features.manage)),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
function resolveNoteListContextType(query: NoteListQuery): SalesDocumentKind | null {
|
|
51
|
+
if (query.contextType) return query.contextType
|
|
52
|
+
if (query.orderId) return 'order'
|
|
53
|
+
if (query.quoteId) return 'quote'
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function requiredNoteFeatures(action: NoteAction, contextType: SalesDocumentKind | null): string[] {
|
|
58
|
+
if (contextType) return [noteFeatureByContext[contextType][action]]
|
|
59
|
+
return action === 'view' ? allNoteViewFeatures : allNoteManageFeatures
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function ensureNotePermission(
|
|
63
|
+
ctx: CrudCtx,
|
|
64
|
+
action: NoteAction,
|
|
65
|
+
contextType: SalesDocumentKind | null,
|
|
66
|
+
translate: (key: string, fallback?: string) => string
|
|
67
|
+
) {
|
|
68
|
+
const auth = ctx.auth
|
|
69
|
+
if (!auth?.sub) {
|
|
70
|
+
throw new CrudHttpError(401, { error: translate('api.errors.unauthorized', 'Unauthorized') })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const requiredFeatures = requiredNoteFeatures(action, contextType)
|
|
74
|
+
const rbac = ctx.container.resolve<RbacService>('rbacService')
|
|
75
|
+
const ok = await rbac.userHasAllFeatures(auth.sub, requiredFeatures, {
|
|
76
|
+
tenantId: auth.tenantId ?? null,
|
|
77
|
+
organizationId: ctx.selectedOrganizationId ?? auth.orgId ?? null,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
if (!ok) {
|
|
81
|
+
throw new CrudHttpError(403, {
|
|
82
|
+
error: translate('api.errors.forbidden', 'Forbidden'),
|
|
83
|
+
requiredFeatures,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function loadNoteForPermission(
|
|
89
|
+
ctx: CrudCtx,
|
|
90
|
+
id: string,
|
|
91
|
+
translate: (key: string, fallback?: string) => string
|
|
92
|
+
): Promise<SalesNote> {
|
|
93
|
+
const em = ctx.container.resolve<EntityManager>('em')
|
|
94
|
+
const note = await findOneWithDecryption(em, SalesNote, { id }, {}, {
|
|
95
|
+
tenantId: ctx.auth?.tenantId ?? undefined,
|
|
96
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? undefined,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
if (!note) {
|
|
100
|
+
throw new CrudHttpError(404, {
|
|
101
|
+
error: translate('sales.documents.detail.error', 'Document not found or inaccessible.'),
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return note
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function ensureExistingNotePermission(
|
|
109
|
+
ctx: CrudCtx,
|
|
110
|
+
id: string,
|
|
111
|
+
action: NoteAction,
|
|
112
|
+
translate: (key: string, fallback?: string) => string
|
|
113
|
+
) {
|
|
114
|
+
const note = await loadNoteForPermission(ctx, id, translate)
|
|
115
|
+
await ensureNotePermission(ctx, action, note.contextType, translate)
|
|
116
|
+
}
|
|
117
|
+
|
|
30
118
|
const routeMetadata = {
|
|
31
119
|
GET: { requireAuth: true },
|
|
32
120
|
POST: { requireAuth: true },
|
|
@@ -47,6 +135,12 @@ const crud = makeCrudRoute({
|
|
|
47
135
|
indexer: {
|
|
48
136
|
entityType: E.sales.sales_note,
|
|
49
137
|
},
|
|
138
|
+
hooks: {
|
|
139
|
+
beforeList: async (query, ctx) => {
|
|
140
|
+
const { translate } = await resolveTranslations()
|
|
141
|
+
await ensureNotePermission(ctx, 'view', resolveNoteListContextType(query as NoteListQuery), translate)
|
|
142
|
+
},
|
|
143
|
+
},
|
|
50
144
|
list: {
|
|
51
145
|
schema: listSchema,
|
|
52
146
|
entityId: E.sales.sales_note,
|
|
@@ -84,7 +178,9 @@ const crud = makeCrudRoute({
|
|
|
84
178
|
schema: rawBodySchema,
|
|
85
179
|
mapInput: async ({ raw, ctx }) => {
|
|
86
180
|
const { translate } = await resolveTranslations()
|
|
87
|
-
|
|
181
|
+
const input = noteCreateSchema.parse(withScopedPayload(raw ?? {}, ctx, translate))
|
|
182
|
+
await ensureNotePermission(ctx, 'manage', input.contextType, translate)
|
|
183
|
+
return input
|
|
88
184
|
},
|
|
89
185
|
response: ({ result }) => ({
|
|
90
186
|
id: result?.noteId ?? result?.id ?? null,
|
|
@@ -97,7 +193,9 @@ const crud = makeCrudRoute({
|
|
|
97
193
|
schema: rawBodySchema,
|
|
98
194
|
mapInput: async ({ raw, ctx }) => {
|
|
99
195
|
const { translate } = await resolveTranslations()
|
|
100
|
-
|
|
196
|
+
const input = noteUpdateSchema.parse(withScopedPayload(raw ?? {}, ctx, translate))
|
|
197
|
+
await ensureExistingNotePermission(ctx, input.id, 'manage', translate)
|
|
198
|
+
return input
|
|
101
199
|
},
|
|
102
200
|
response: () => ({ ok: true }),
|
|
103
201
|
},
|
|
@@ -114,6 +212,7 @@ const crud = makeCrudRoute({
|
|
|
114
212
|
if (!id) {
|
|
115
213
|
throw new CrudHttpError(400, { error: translate('sales.documents.detail.error', 'Document not found or inaccessible.') })
|
|
116
214
|
}
|
|
215
|
+
await ensureExistingNotePermission(ctx, id, 'manage', translate)
|
|
117
216
|
return { id }
|
|
118
217
|
},
|
|
119
218
|
response: () => ({ ok: true }),
|
|
@@ -4738,10 +4738,14 @@ const createQuoteCommand: CommandHandler<
|
|
|
4738
4738
|
linkHref: `/backend/sales/quotes/${quote.id}`,
|
|
4739
4739
|
});
|
|
4740
4740
|
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4741
|
+
// Bulk-import backfills opt out of the per-record notification fan-out (and its inline
|
|
4742
|
+
// e-mail delivery); interactive creates are unaffected.
|
|
4743
|
+
if (!ctx.bulkImport?.skipNotifications) {
|
|
4744
|
+
await notificationService.createForFeature(notificationInput, {
|
|
4745
|
+
tenantId: quote.tenantId,
|
|
4746
|
+
organizationId: quote.organizationId ?? null,
|
|
4747
|
+
});
|
|
4748
|
+
}
|
|
4745
4749
|
}
|
|
4746
4750
|
} catch (err) {
|
|
4747
4751
|
// Notification creation is non-critical, don't fail the command
|
|
@@ -5741,10 +5745,14 @@ const createOrderCommand: CommandHandler<
|
|
|
5741
5745
|
linkHref: `/backend/sales/orders/${order.id}`,
|
|
5742
5746
|
});
|
|
5743
5747
|
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
+
// Bulk-import backfills opt out of the per-record notification fan-out (and its inline
|
|
5749
|
+
// e-mail delivery); interactive creates are unaffected.
|
|
5750
|
+
if (!ctx.bulkImport?.skipNotifications) {
|
|
5751
|
+
await notificationService.createForFeature(notificationInput, {
|
|
5752
|
+
tenantId: order.tenantId,
|
|
5753
|
+
organizationId: order.organizationId ?? null,
|
|
5754
|
+
});
|
|
5755
|
+
}
|
|
5748
5756
|
}
|
|
5749
5757
|
} catch (err) {
|
|
5750
5758
|
// Notification creation is non-critical, don't fail the command
|
|
@@ -124,7 +124,13 @@ async function requireContext(
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
const repo = contextType === 'invoice' ? SalesInvoice : SalesCreditMemo
|
|
127
|
-
const entity = await
|
|
127
|
+
const entity = await findOneWithDecryption(
|
|
128
|
+
em,
|
|
129
|
+
repo as any,
|
|
130
|
+
{ id: contextId },
|
|
131
|
+
{},
|
|
132
|
+
{ tenantId, organizationId },
|
|
133
|
+
) as (SalesInvoice | SalesCreditMemo) | null
|
|
128
134
|
if (!entity) {
|
|
129
135
|
throw new CrudHttpError(404, { error: 'sales.notes.context_not_found' })
|
|
130
136
|
}
|
|
@@ -139,11 +145,11 @@ async function requireContext(
|
|
|
139
145
|
}
|
|
140
146
|
}
|
|
141
147
|
|
|
142
|
-
function resolveAuthor(
|
|
143
|
-
|
|
144
|
-
if (!
|
|
148
|
+
function resolveAuthor(authSub: string | null): string | null {
|
|
149
|
+
const sub = authSub?.trim()
|
|
150
|
+
if (!sub) return null
|
|
145
151
|
const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/
|
|
146
|
-
return uuidRegex.test(
|
|
152
|
+
return uuidRegex.test(sub) ? sub : null
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
const createNoteCommand: CommandHandler<NoteCreateInput, { noteId: string; authorUserId: string | null }> = {
|
|
@@ -154,7 +160,7 @@ const createNoteCommand: CommandHandler<NoteCreateInput, { noteId: string; autho
|
|
|
154
160
|
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
155
161
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
156
162
|
const context = await requireContext(em, parsed.contextType, parsed.contextId, parsed.organizationId, parsed.tenantId)
|
|
157
|
-
const authorUserId = resolveAuthor(
|
|
163
|
+
const authorUserId = resolveAuthor(ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null)
|
|
158
164
|
|
|
159
165
|
const note = em.create(SalesNote, {
|
|
160
166
|
organizationId: parsed.organizationId,
|
|
@@ -278,7 +284,9 @@ const updateNoteCommand: CommandHandler<NoteUpdateInput, { noteId: string }> = {
|
|
|
278
284
|
ensureOrganizationScope(ctx, note.organizationId)
|
|
279
285
|
|
|
280
286
|
if (parsed.body !== undefined) note.body = parsed.body
|
|
281
|
-
if (parsed.authorUserId !== undefined)
|
|
287
|
+
if (parsed.authorUserId !== undefined) {
|
|
288
|
+
note.authorUserId = resolveAuthor(ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null)
|
|
289
|
+
}
|
|
282
290
|
if (parsed.appearanceIcon !== undefined) note.appearanceIcon = parsed.appearanceIcon ?? null
|
|
283
291
|
if (parsed.appearanceColor !== undefined) note.appearanceColor = parsed.appearanceColor ?? null
|
|
284
292
|
note.updatedAt = new Date()
|