@open-mercato/core 0.4.6-develop-bbfd75b1c8 → 0.4.6-develop-2ba4e02ffb

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 (37) hide show
  1. package/dist/modules/data_sync/api/mappings/[id]/route.js +2 -2
  2. package/dist/modules/data_sync/api/mappings/[id]/route.js.map +2 -2
  3. package/dist/modules/data_sync/api/mappings/route.js +2 -2
  4. package/dist/modules/data_sync/api/mappings/route.js.map +2 -2
  5. package/dist/modules/integrations/api/[id]/route.js +9 -1
  6. package/dist/modules/integrations/api/[id]/route.js.map +2 -2
  7. package/dist/modules/integrations/api/logs/route.js +2 -2
  8. package/dist/modules/integrations/api/logs/route.js.map +2 -2
  9. package/dist/modules/integrations/backend/integrations/[id]/page.js +5 -1
  10. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  11. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +5 -1
  12. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
  13. package/dist/modules/integrations/lib/credentials-service.js +5 -1
  14. package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
  15. package/dist/modules/integrations/lib/health-service.js.map +2 -2
  16. package/dist/modules/integrations/lib/log-service.js.map +2 -2
  17. package/dist/modules/integrations/lib/state-service.js.map +2 -2
  18. package/dist/modules/sales/api/quotes/convert/route.js +31 -14
  19. package/dist/modules/sales/api/quotes/convert/route.js.map +2 -2
  20. package/dist/modules/sales/api/quotes/send/route.js +31 -14
  21. package/dist/modules/sales/api/quotes/send/route.js.map +2 -2
  22. package/package.json +2 -2
  23. package/src/modules/data_sync/api/mappings/[id]/route.ts +2 -2
  24. package/src/modules/data_sync/api/mappings/route.ts +2 -2
  25. package/src/modules/integrations/api/[id]/route.ts +9 -1
  26. package/src/modules/integrations/api/logs/route.ts +3 -3
  27. package/src/modules/integrations/backend/integrations/[id]/page.tsx +8 -2
  28. package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +8 -2
  29. package/src/modules/integrations/lib/credentials-service.ts +6 -2
  30. package/src/modules/integrations/lib/health-service.ts +1 -2
  31. package/src/modules/integrations/lib/log-service.ts +1 -1
  32. package/src/modules/integrations/lib/state-service.ts +1 -1
  33. package/src/modules/sales/api/quotes/convert/route.ts +55 -14
  34. package/src/modules/sales/api/quotes/send/route.ts +55 -14
  35. package/dist/modules/integrations/lib/types.js +0 -1
  36. package/dist/modules/integrations/lib/types.js.map +0 -7
  37. package/src/modules/integrations/lib/types.ts +0 -4
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/integrations/lib/credentials-service.ts"],
4
- "sourcesContent": ["import crypto from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { decryptWithAesGcm, encryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createKmsService } from '@open-mercato/shared/lib/encryption/kms'\nimport { getBundle, getIntegration, resolveIntegrationCredentialsSchema } from '@open-mercato/shared/modules/integrations/types'\nimport { EncryptionMap } from '../../entities/data/entities'\nimport { IntegrationCredentials } from '../data/entities'\nimport type { IntegrationScope } from './types'\n\nconst ENCRYPTED_CREDENTIALS_BLOB_KEY = '__om_encrypted_credentials_blob_v1'\nconst DERIVED_KEY_CONTEXT = 'integrations.credentials'\n\nfunction resolveFallbackEncryptionSecret(): string {\n const candidates = [\n process.env.TENANT_DATA_ENCRYPTION_FALLBACK_KEY,\n process.env.TENANT_DATA_ENCRYPTION_KEY,\n process.env.AUTH_SECRET,\n process.env.NEXTAUTH_SECRET,\n ]\n\n for (const value of candidates) {\n const normalized = value?.trim()\n if (normalized) return normalized\n }\n\n if (process.env.NODE_ENV !== 'production') return 'om-dev-tenant-encryption'\n\n console.warn(\n '[integrations.credentials] No encryption secret configured; using emergency fallback secret. Configure TENANT_DATA_ENCRYPTION_FALLBACK_KEY immediately.',\n )\n return 'om-emergency-fallback-rotate-me'\n}\n\nfunction deriveDekFromSecret(secret: string, tenantId: string): string {\n return crypto\n .createHash('sha256')\n .update(`${DERIVED_KEY_CONTEXT}:${tenantId}:${secret}`)\n .digest()\n .toString('base64')\n}\n\nexport function createCredentialsService(em: EntityManager) {\n const credentialsEncryptionSpec = [{ field: 'credentials' }]\n\n async function ensureCredentialsEncryptionMap(scope: IntegrationScope): Promise<void> {\n const existing = await findOneWithDecryption(\n em,\n EncryptionMap,\n {\n entityId: 'integrations:integration_credentials',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (!existing) {\n const created = em.create(EncryptionMap, {\n entityId: 'integrations:integration_credentials',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n fieldsJson: credentialsEncryptionSpec,\n isActive: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(created)\n return\n }\n\n existing.fieldsJson = credentialsEncryptionSpec\n existing.isActive = true\n }\n\n async function resolveCredentialsDek(scope: IntegrationScope): Promise<string> {\n const kms = createKmsService()\n const existing = await kms.getTenantDek(scope.tenantId)\n if (existing?.key) return existing.key\n\n const created = await kms.createTenantDek(scope.tenantId)\n if (created?.key) return created.key\n\n return deriveDekFromSecret(resolveFallbackEncryptionSecret(), scope.tenantId)\n }\n\n async function encryptCredentialsBlob(\n credentials: Record<string, unknown>,\n scope: IntegrationScope,\n ): Promise<Record<string, unknown>> {\n const dek = await resolveCredentialsDek(scope)\n const payload = encryptWithAesGcm(JSON.stringify(credentials), dek)\n return { [ENCRYPTED_CREDENTIALS_BLOB_KEY]: payload.value }\n }\n\n async function decryptCredentialsBlob(\n credentials: Record<string, unknown>,\n scope: IntegrationScope,\n ): Promise<Record<string, unknown>> {\n const encrypted = credentials[ENCRYPTED_CREDENTIALS_BLOB_KEY]\n if (typeof encrypted !== 'string' || !encrypted) return credentials\n\n const dek = await resolveCredentialsDek(scope)\n const decryptedRaw = decryptWithAesGcm(encrypted, dek)\n if (!decryptedRaw) return {}\n\n try {\n const parsed = JSON.parse(decryptedRaw) as unknown\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {}\n } catch {\n return {}\n }\n }\n\n return {\n async getRaw(integrationId: string, scope: IntegrationScope): Promise<Record<string, unknown> | null> {\n const row = await findOneWithDecryption(\n em,\n IntegrationCredentials,\n {\n integrationId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!row) return null\n return decryptCredentialsBlob(row.credentials, scope)\n },\n\n async resolve(integrationId: string, scope: IntegrationScope): Promise<Record<string, unknown> | null> {\n const direct = await this.getRaw(integrationId, scope)\n if (direct) return direct\n\n const definition = getIntegration(integrationId)\n if (!definition?.bundleId) return null\n return this.getRaw(definition.bundleId, scope)\n },\n\n async save(integrationId: string, credentials: Record<string, unknown>, scope: IntegrationScope): Promise<void> {\n await ensureCredentialsEncryptionMap(scope)\n const encryptedCredentials = await encryptCredentialsBlob(credentials, scope)\n\n const row = await findOneWithDecryption(\n em,\n IntegrationCredentials,\n {\n integrationId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (row) {\n row.credentials = encryptedCredentials\n await em.flush()\n return\n }\n\n const created = em.create(IntegrationCredentials, {\n integrationId,\n credentials: encryptedCredentials,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n await em.persistAndFlush(created)\n },\n\n async saveField(\n integrationId: string,\n fieldKey: string,\n value: unknown,\n scope: IntegrationScope,\n ): Promise<Record<string, unknown>> {\n const current = (await this.getRaw(integrationId, scope)) ?? {}\n const updated = { ...current, [fieldKey]: value }\n await this.save(integrationId, updated, scope)\n return updated\n },\n\n getSchema(integrationId: string) {\n const definition = getIntegration(integrationId)\n if (!definition) return undefined\n\n if (definition.bundleId) {\n const bundle = getBundle(definition.bundleId)\n return bundle?.credentials ?? resolveIntegrationCredentialsSchema(integrationId)\n }\n\n return definition.credentials ?? resolveIntegrationCredentialsSchema(integrationId)\n },\n }\n}\n\nexport type CredentialsService = ReturnType<typeof createCredentialsService>\n"],
5
- "mappings": "AAAA,OAAO,YAAY;AAEnB,SAAS,mBAAmB,yBAAyB;AACrD,SAAS,6BAA6B;AACtC,SAAS,wBAAwB;AACjC,SAAS,WAAW,gBAAgB,2CAA2C;AAC/E,SAAS,qBAAqB;AAC9B,SAAS,8BAA8B;AAGvC,MAAM,iCAAiC;AACvC,MAAM,sBAAsB;AAE5B,SAAS,kCAA0C;AACjD,QAAM,aAAa;AAAA,IACjB,QAAQ,IAAI;AAAA,IACZ,QAAQ,IAAI;AAAA,IACZ,QAAQ,IAAI;AAAA,IACZ,QAAQ,IAAI;AAAA,EACd;AAEA,aAAW,SAAS,YAAY;AAC9B,UAAM,aAAa,OAAO,KAAK;AAC/B,QAAI,WAAY,QAAO;AAAA,EACzB;AAEA,MAAI,QAAQ,IAAI,aAAa,aAAc,QAAO;AAElD,UAAQ;AAAA,IACN;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,oBAAoB,QAAgB,UAA0B;AACrE,SAAO,OACJ,WAAW,QAAQ,EACnB,OAAO,GAAG,mBAAmB,IAAI,QAAQ,IAAI,MAAM,EAAE,EACrD,OAAO,EACP,SAAS,QAAQ;AACtB;AAEO,SAAS,yBAAyB,IAAmB;AAC1D,QAAM,4BAA4B,CAAC,EAAE,OAAO,cAAc,CAAC;AAE3D,iBAAe,+BAA+B,OAAwC;AACpF,UAAM,WAAW,MAAM;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,QACE,UAAU;AAAA,QACV,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,WAAW;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,QAAI,CAAC,UAAU;AACb,YAAM,UAAU,GAAG,OAAO,eAAe;AAAA,QACvC,UAAU;AAAA,QACV,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,YAAY;AAAA,QACZ,UAAU;AAAA,QACV,WAAW,oBAAI,KAAK;AAAA,QACpB,WAAW,oBAAI,KAAK;AAAA,MACtB,CAAC;AACD,SAAG,QAAQ,OAAO;AAClB;AAAA,IACF;AAEA,aAAS,aAAa;AACtB,aAAS,WAAW;AAAA,EACtB;AAEA,iBAAe,sBAAsB,OAA0C;AAC7E,UAAM,MAAM,iBAAiB;AAC7B,UAAM,WAAW,MAAM,IAAI,aAAa,MAAM,QAAQ;AACtD,QAAI,UAAU,IAAK,QAAO,SAAS;AAEnC,UAAM,UAAU,MAAM,IAAI,gBAAgB,MAAM,QAAQ;AACxD,QAAI,SAAS,IAAK,QAAO,QAAQ;AAEjC,WAAO,oBAAoB,gCAAgC,GAAG,MAAM,QAAQ;AAAA,EAC9E;AAEA,iBAAe,uBACb,aACA,OACkC;AAClC,UAAM,MAAM,MAAM,sBAAsB,KAAK;AAC7C,UAAM,UAAU,kBAAkB,KAAK,UAAU,WAAW,GAAG,GAAG;AAClE,WAAO,EAAE,CAAC,8BAA8B,GAAG,QAAQ,MAAM;AAAA,EAC3D;AAEA,iBAAe,uBACb,aACA,OACkC;AAClC,UAAM,YAAY,YAAY,8BAA8B;AAC5D,QAAI,OAAO,cAAc,YAAY,CAAC,UAAW,QAAO;AAExD,UAAM,MAAM,MAAM,sBAAsB,KAAK;AAC7C,UAAM,eAAe,kBAAkB,WAAW,GAAG;AACrD,QAAI,CAAC,aAAc,QAAO,CAAC;AAE3B,QAAI;AACF,YAAM,SAAS,KAAK,MAAM,YAAY;AACtC,aAAO,UAAU,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,IAC/D,SACD,CAAC;AAAA,IACP,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM,OAAO,eAAuB,OAAkE;AACpG,YAAM,MAAM,MAAM;AAAA,QAChB;AAAA,QACA;AAAA,QACA;AAAA,UACE;AAAA,UACA,gBAAgB,MAAM;AAAA,UACtB,UAAU,MAAM;AAAA,UAChB,WAAW;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,UAAI,CAAC,IAAK,QAAO;AACjB,aAAO,uBAAuB,IAAI,aAAa,KAAK;AAAA,IACtD;AAAA,IAEA,MAAM,QAAQ,eAAuB,OAAkE;AACrG,YAAM,SAAS,MAAM,KAAK,OAAO,eAAe,KAAK;AACrD,UAAI,OAAQ,QAAO;AAEnB,YAAM,aAAa,eAAe,aAAa;AAC/C,UAAI,CAAC,YAAY,SAAU,QAAO;AAClC,aAAO,KAAK,OAAO,WAAW,UAAU,KAAK;AAAA,IAC/C;AAAA,IAEA,MAAM,KAAK,eAAuB,aAAsC,OAAwC;AAC9G,YAAM,+BAA+B,KAAK;AAC1C,YAAM,uBAAuB,MAAM,uBAAuB,aAAa,KAAK;AAE5E,YAAM,MAAM,MAAM;AAAA,QAChB;AAAA,QACA;AAAA,QACA;AAAA,UACE;AAAA,UACA,gBAAgB,MAAM;AAAA,UACtB,UAAU,MAAM;AAAA,UAChB,WAAW;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAEA,UAAI,KAAK;AACP,YAAI,cAAc;AAClB,cAAM,GAAG,MAAM;AACf;AAAA,MACF;AAEA,YAAM,UAAU,GAAG,OAAO,wBAAwB;AAAA,QAChD;AAAA,QACA,aAAa;AAAA,QACb,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,MAClB,CAAC;AACD,YAAM,GAAG,gBAAgB,OAAO;AAAA,IAClC;AAAA,IAEA,MAAM,UACJ,eACA,UACA,OACA,OACkC;AAClC,YAAM,UAAW,MAAM,KAAK,OAAO,eAAe,KAAK,KAAM,CAAC;AAC9D,YAAM,UAAU,EAAE,GAAG,SAAS,CAAC,QAAQ,GAAG,MAAM;AAChD,YAAM,KAAK,KAAK,eAAe,SAAS,KAAK;AAC7C,aAAO;AAAA,IACT;AAAA,IAEA,UAAU,eAAuB;AAC/B,YAAM,aAAa,eAAe,aAAa;AAC/C,UAAI,CAAC,WAAY,QAAO;AAExB,UAAI,WAAW,UAAU;AACvB,cAAM,SAAS,UAAU,WAAW,QAAQ;AAC5C,eAAO,QAAQ,eAAe,oCAAoC,aAAa;AAAA,MACjF;AAEA,aAAO,WAAW,eAAe,oCAAoC,aAAa;AAAA,IACpF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import crypto from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { decryptWithAesGcm, encryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createKmsService } from '@open-mercato/shared/lib/encryption/kms'\nimport {\n getBundle,\n getIntegration,\n resolveIntegrationCredentialsSchema,\n type IntegrationScope,\n} from '@open-mercato/shared/modules/integrations/types'\nimport { EncryptionMap } from '../../entities/data/entities'\nimport { IntegrationCredentials } from '../data/entities'\n\nconst ENCRYPTED_CREDENTIALS_BLOB_KEY = '__om_encrypted_credentials_blob_v1'\nconst DERIVED_KEY_CONTEXT = 'integrations.credentials'\n\nfunction resolveFallbackEncryptionSecret(): string {\n const candidates = [\n process.env.TENANT_DATA_ENCRYPTION_FALLBACK_KEY,\n process.env.TENANT_DATA_ENCRYPTION_KEY,\n process.env.AUTH_SECRET,\n process.env.NEXTAUTH_SECRET,\n ]\n\n for (const value of candidates) {\n const normalized = value?.trim()\n if (normalized) return normalized\n }\n\n if (process.env.NODE_ENV !== 'production') return 'om-dev-tenant-encryption'\n\n console.warn(\n '[integrations.credentials] No encryption secret configured; using emergency fallback secret. Configure TENANT_DATA_ENCRYPTION_FALLBACK_KEY immediately.',\n )\n return 'om-emergency-fallback-rotate-me'\n}\n\nfunction deriveDekFromSecret(secret: string, tenantId: string): string {\n return crypto\n .createHash('sha256')\n .update(`${DERIVED_KEY_CONTEXT}:${tenantId}:${secret}`)\n .digest()\n .toString('base64')\n}\n\nexport function createCredentialsService(em: EntityManager) {\n const credentialsEncryptionSpec = [{ field: 'credentials' }]\n\n async function ensureCredentialsEncryptionMap(scope: IntegrationScope): Promise<void> {\n const existing = await findOneWithDecryption(\n em,\n EncryptionMap,\n {\n entityId: 'integrations:integration_credentials',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (!existing) {\n const created = em.create(EncryptionMap, {\n entityId: 'integrations:integration_credentials',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n fieldsJson: credentialsEncryptionSpec,\n isActive: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(created)\n return\n }\n\n existing.fieldsJson = credentialsEncryptionSpec\n existing.isActive = true\n }\n\n async function resolveCredentialsDek(scope: IntegrationScope): Promise<string> {\n const kms = createKmsService()\n const existing = await kms.getTenantDek(scope.tenantId)\n if (existing?.key) return existing.key\n\n const created = await kms.createTenantDek(scope.tenantId)\n if (created?.key) return created.key\n\n return deriveDekFromSecret(resolveFallbackEncryptionSecret(), scope.tenantId)\n }\n\n async function encryptCredentialsBlob(\n credentials: Record<string, unknown>,\n scope: IntegrationScope,\n ): Promise<Record<string, unknown>> {\n const dek = await resolveCredentialsDek(scope)\n const payload = encryptWithAesGcm(JSON.stringify(credentials), dek)\n return { [ENCRYPTED_CREDENTIALS_BLOB_KEY]: payload.value }\n }\n\n async function decryptCredentialsBlob(\n credentials: Record<string, unknown>,\n scope: IntegrationScope,\n ): Promise<Record<string, unknown>> {\n const encrypted = credentials[ENCRYPTED_CREDENTIALS_BLOB_KEY]\n if (typeof encrypted !== 'string' || !encrypted) return credentials\n\n const dek = await resolveCredentialsDek(scope)\n const decryptedRaw = decryptWithAesGcm(encrypted, dek)\n if (!decryptedRaw) return {}\n\n try {\n const parsed = JSON.parse(decryptedRaw) as unknown\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {}\n } catch {\n return {}\n }\n }\n\n return {\n async getRaw(integrationId: string, scope: IntegrationScope): Promise<Record<string, unknown> | null> {\n const row = await findOneWithDecryption(\n em,\n IntegrationCredentials,\n {\n integrationId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!row) return null\n return decryptCredentialsBlob(row.credentials, scope)\n },\n\n async resolve(integrationId: string, scope: IntegrationScope): Promise<Record<string, unknown> | null> {\n const direct = await this.getRaw(integrationId, scope)\n if (direct) return direct\n\n const definition = getIntegration(integrationId)\n if (!definition?.bundleId) return null\n return this.getRaw(definition.bundleId, scope)\n },\n\n async save(integrationId: string, credentials: Record<string, unknown>, scope: IntegrationScope): Promise<void> {\n await ensureCredentialsEncryptionMap(scope)\n const encryptedCredentials = await encryptCredentialsBlob(credentials, scope)\n\n const row = await findOneWithDecryption(\n em,\n IntegrationCredentials,\n {\n integrationId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (row) {\n row.credentials = encryptedCredentials\n await em.flush()\n return\n }\n\n const created = em.create(IntegrationCredentials, {\n integrationId,\n credentials: encryptedCredentials,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n await em.persistAndFlush(created)\n },\n\n async saveField(\n integrationId: string,\n fieldKey: string,\n value: unknown,\n scope: IntegrationScope,\n ): Promise<Record<string, unknown>> {\n const current = (await this.getRaw(integrationId, scope)) ?? {}\n const updated = { ...current, [fieldKey]: value }\n await this.save(integrationId, updated, scope)\n return updated\n },\n\n getSchema(integrationId: string) {\n const definition = getIntegration(integrationId)\n if (!definition) return undefined\n\n if (definition.bundleId) {\n const bundle = getBundle(definition.bundleId)\n return bundle?.credentials ?? resolveIntegrationCredentialsSchema(integrationId)\n }\n\n return definition.credentials ?? resolveIntegrationCredentialsSchema(integrationId)\n },\n }\n}\n\nexport type CredentialsService = ReturnType<typeof createCredentialsService>\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/integrations/lib/health-service.ts"],
4
- "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { IntegrationStateService } from './state-service'\nimport type { IntegrationLogService } from './log-service'\nimport { getIntegration, getBundle } from '@open-mercato/shared/modules/integrations/types'\nimport type { IntegrationScope } from './types'\n\ntype HealthCheckResult = {\n status: 'healthy' | 'degraded' | 'unhealthy'\n message?: string\n details?: Record<string, unknown>\n}\n\ntype HealthCheckService = {\n check: (credentials: Record<string, unknown> | null, scope: IntegrationScope) => Promise<HealthCheckResult>\n}\n\nexport function createHealthService(\n container: AwilixContainer,\n stateService: IntegrationStateService,\n logService: IntegrationLogService,\n) {\n return {\n async runHealthCheck(integrationId: string, scope: IntegrationScope): Promise<HealthCheckResult> {\n const definition = getIntegration(integrationId)\n const healthConfig = definition?.healthCheck ?? (definition?.bundleId ? getBundle(definition.bundleId)?.healthCheck : undefined)\n\n if (!healthConfig?.service) {\n return { status: 'unhealthy', message: 'No health check configured' }\n }\n\n let result: HealthCheckResult\n try {\n const checker = container.resolve<HealthCheckService>(healthConfig.service)\n const credentialsService = container.resolve<{ resolve: (id: string, scope: IntegrationScope) => Promise<Record<string, unknown> | null> }>('integrationCredentialsService')\n const credentials = await credentialsService.resolve(integrationId, scope)\n result = await checker.check(credentials, scope)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Health check failed'\n result = { status: 'unhealthy', message }\n }\n\n await stateService.upsert(integrationId, {\n lastHealthStatus: result.status,\n lastHealthCheckedAt: new Date(),\n }, scope)\n\n const logger = logService.scoped(integrationId, scope)\n if (result.status === 'healthy') {\n await logger.info(`Health check passed`, { status: result.status, ...result.details })\n } else {\n await logger.warn(`Health check: ${result.status}`, { status: result.status, message: result.message, ...result.details })\n }\n\n return result\n },\n }\n}\n\nexport type IntegrationHealthService = ReturnType<typeof createHealthService>\n"],
5
- "mappings": "AAGA,SAAS,gBAAgB,iBAAiB;AAanC,SAAS,oBACd,WACA,cACA,YACA;AACA,SAAO;AAAA,IACL,MAAM,eAAe,eAAuB,OAAqD;AAC/F,YAAM,aAAa,eAAe,aAAa;AAC/C,YAAM,eAAe,YAAY,gBAAgB,YAAY,WAAW,UAAU,WAAW,QAAQ,GAAG,cAAc;AAEtH,UAAI,CAAC,cAAc,SAAS;AAC1B,eAAO,EAAE,QAAQ,aAAa,SAAS,6BAA6B;AAAA,MACtE;AAEA,UAAI;AACJ,UAAI;AACF,cAAM,UAAU,UAAU,QAA4B,aAAa,OAAO;AAC1E,cAAM,qBAAqB,UAAU,QAAuG,+BAA+B;AAC3K,cAAM,cAAc,MAAM,mBAAmB,QAAQ,eAAe,KAAK;AACzE,iBAAS,MAAM,QAAQ,MAAM,aAAa,KAAK;AAAA,MACjD,SAAS,OAAO;AACd,cAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,iBAAS,EAAE,QAAQ,aAAa,QAAQ;AAAA,MAC1C;AAEA,YAAM,aAAa,OAAO,eAAe;AAAA,QACvC,kBAAkB,OAAO;AAAA,QACzB,qBAAqB,oBAAI,KAAK;AAAA,MAChC,GAAG,KAAK;AAER,YAAM,SAAS,WAAW,OAAO,eAAe,KAAK;AACrD,UAAI,OAAO,WAAW,WAAW;AAC/B,cAAM,OAAO,KAAK,uBAAuB,EAAE,QAAQ,OAAO,QAAQ,GAAG,OAAO,QAAQ,CAAC;AAAA,MACvF,OAAO;AACL,cAAM,OAAO,KAAK,iBAAiB,OAAO,MAAM,IAAI,EAAE,QAAQ,OAAO,QAAQ,SAAS,OAAO,SAAS,GAAG,OAAO,QAAQ,CAAC;AAAA,MAC3H;AAEA,aAAO;AAAA,IACT;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { IntegrationStateService } from './state-service'\nimport type { IntegrationLogService } from './log-service'\nimport { getIntegration, getBundle, type IntegrationScope } from '@open-mercato/shared/modules/integrations/types'\n\ntype HealthCheckResult = {\n status: 'healthy' | 'degraded' | 'unhealthy'\n message?: string\n details?: Record<string, unknown>\n}\n\ntype HealthCheckService = {\n check: (credentials: Record<string, unknown> | null, scope: IntegrationScope) => Promise<HealthCheckResult>\n}\n\nexport function createHealthService(\n container: AwilixContainer,\n stateService: IntegrationStateService,\n logService: IntegrationLogService,\n) {\n return {\n async runHealthCheck(integrationId: string, scope: IntegrationScope): Promise<HealthCheckResult> {\n const definition = getIntegration(integrationId)\n const healthConfig = definition?.healthCheck ?? (definition?.bundleId ? getBundle(definition.bundleId)?.healthCheck : undefined)\n\n if (!healthConfig?.service) {\n return { status: 'unhealthy', message: 'No health check configured' }\n }\n\n let result: HealthCheckResult\n try {\n const checker = container.resolve<HealthCheckService>(healthConfig.service)\n const credentialsService = container.resolve<{ resolve: (id: string, scope: IntegrationScope) => Promise<Record<string, unknown> | null> }>('integrationCredentialsService')\n const credentials = await credentialsService.resolve(integrationId, scope)\n result = await checker.check(credentials, scope)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Health check failed'\n result = { status: 'unhealthy', message }\n }\n\n await stateService.upsert(integrationId, {\n lastHealthStatus: result.status,\n lastHealthCheckedAt: new Date(),\n }, scope)\n\n const logger = logService.scoped(integrationId, scope)\n if (result.status === 'healthy') {\n await logger.info(`Health check passed`, { status: result.status, ...result.details })\n } else {\n await logger.warn(`Health check: ${result.status}`, { status: result.status, message: result.message, ...result.details })\n }\n\n return result\n },\n }\n}\n\nexport type IntegrationHealthService = ReturnType<typeof createHealthService>\n"],
5
+ "mappings": "AAGA,SAAS,gBAAgB,iBAAwC;AAY1D,SAAS,oBACd,WACA,cACA,YACA;AACA,SAAO;AAAA,IACL,MAAM,eAAe,eAAuB,OAAqD;AAC/F,YAAM,aAAa,eAAe,aAAa;AAC/C,YAAM,eAAe,YAAY,gBAAgB,YAAY,WAAW,UAAU,WAAW,QAAQ,GAAG,cAAc;AAEtH,UAAI,CAAC,cAAc,SAAS;AAC1B,eAAO,EAAE,QAAQ,aAAa,SAAS,6BAA6B;AAAA,MACtE;AAEA,UAAI;AACJ,UAAI;AACF,cAAM,UAAU,UAAU,QAA4B,aAAa,OAAO;AAC1E,cAAM,qBAAqB,UAAU,QAAuG,+BAA+B;AAC3K,cAAM,cAAc,MAAM,mBAAmB,QAAQ,eAAe,KAAK;AACzE,iBAAS,MAAM,QAAQ,MAAM,aAAa,KAAK;AAAA,MACjD,SAAS,OAAO;AACd,cAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,iBAAS,EAAE,QAAQ,aAAa,QAAQ;AAAA,MAC1C;AAEA,YAAM,aAAa,OAAO,eAAe;AAAA,QACvC,kBAAkB,OAAO;AAAA,QACzB,qBAAqB,oBAAI,KAAK;AAAA,MAChC,GAAG,KAAK;AAER,YAAM,SAAS,WAAW,OAAO,eAAe,KAAK;AACrD,UAAI,OAAO,WAAW,WAAW;AAC/B,cAAM,OAAO,KAAK,uBAAuB,EAAE,QAAQ,OAAO,QAAQ,GAAG,OAAO,QAAQ,CAAC;AAAA,MACvF,OAAO;AACL,cAAM,OAAO,KAAK,iBAAiB,OAAO,MAAM,IAAI,EAAE,QAAQ,OAAO,QAAQ,SAAS,OAAO,SAAS,GAAG,OAAO,QAAQ,CAAC;AAAA,MAC3H;AAEA,aAAO;AAAA,IACT;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/integrations/lib/log-service.ts"],
4
- "sourcesContent": ["import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { ListIntegrationLogsQuery } from '../data/validators'\nimport { IntegrationLog } from '../data/entities'\nimport type { IntegrationScope } from './types'\n\ntype LogInput = {\n integrationId: string\n runId?: string | null\n scopeEntityType?: string | null\n scopeEntityId?: string | null\n level: 'info' | 'warn' | 'error'\n message: string\n code?: string | null\n payload?: Record<string, unknown> | null\n}\n\nexport function createIntegrationLogService(em: EntityManager) {\n return {\n async write(input: LogInput, scope: IntegrationScope): Promise<IntegrationLog> {\n const row = em.create(IntegrationLog, {\n integrationId: input.integrationId,\n runId: input.runId,\n scopeEntityType: input.scopeEntityType,\n scopeEntityId: input.scopeEntityId,\n level: input.level,\n message: input.message,\n code: input.code,\n payload: input.payload,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n await em.persistAndFlush(row)\n return row\n },\n\n scoped(integrationId: string, scope: IntegrationScope) {\n return {\n info: (message: string, payload?: Record<string, unknown>) => this.write({ integrationId, level: 'info', message, payload }, scope),\n warn: (message: string, payload?: Record<string, unknown>) => this.write({ integrationId, level: 'warn', message, payload }, scope),\n error: (message: string, payload?: Record<string, unknown>) => this.write({ integrationId, level: 'error', message, payload }, scope),\n }\n },\n\n async query(query: ListIntegrationLogsQuery, scope: IntegrationScope): Promise<{ items: IntegrationLog[]; total: number }> {\n const where: FilterQuery<IntegrationLog> = {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n }\n\n if (query.integrationId) where.integrationId = query.integrationId\n if (query.level) where.level = query.level\n if (query.runId) where.runId = query.runId\n if (query.entityType) where.scopeEntityType = query.entityType\n if (query.entityId) where.scopeEntityId = query.entityId\n\n const items = await findWithDecryption(\n em,\n IntegrationLog,\n where,\n {\n orderBy: { createdAt: 'DESC' },\n limit: query.pageSize,\n offset: (query.page - 1) * query.pageSize,\n },\n scope,\n )\n const total = await em.count(IntegrationLog, where)\n return { items, total }\n },\n\n async pruneOlderThan(days: number, scope: IntegrationScope): Promise<number> {\n const threshold = new Date(Date.now() - days * 24 * 60 * 60 * 1000)\n const deletedCount = await em.nativeDelete(IntegrationLog, {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n createdAt: { $lt: threshold },\n })\n return deletedCount\n },\n }\n}\n\nexport type IntegrationLogService = ReturnType<typeof createIntegrationLogService>\n"],
5
- "mappings": "AACA,SAAS,0BAA0B;AAEnC,SAAS,sBAAsB;AAcxB,SAAS,4BAA4B,IAAmB;AAC7D,SAAO;AAAA,IACL,MAAM,MAAM,OAAiB,OAAkD;AAC7E,YAAM,MAAM,GAAG,OAAO,gBAAgB;AAAA,QACpC,eAAe,MAAM;AAAA,QACrB,OAAO,MAAM;AAAA,QACb,iBAAiB,MAAM;AAAA,QACvB,eAAe,MAAM;AAAA,QACrB,OAAO,MAAM;AAAA,QACb,SAAS,MAAM;AAAA,QACf,MAAM,MAAM;AAAA,QACZ,SAAS,MAAM;AAAA,QACf,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,MAClB,CAAC;AACD,YAAM,GAAG,gBAAgB,GAAG;AAC5B,aAAO;AAAA,IACT;AAAA,IAEA,OAAO,eAAuB,OAAyB;AACrD,aAAO;AAAA,QACL,MAAM,CAAC,SAAiB,YAAsC,KAAK,MAAM,EAAE,eAAe,OAAO,QAAQ,SAAS,QAAQ,GAAG,KAAK;AAAA,QAClI,MAAM,CAAC,SAAiB,YAAsC,KAAK,MAAM,EAAE,eAAe,OAAO,QAAQ,SAAS,QAAQ,GAAG,KAAK;AAAA,QAClI,OAAO,CAAC,SAAiB,YAAsC,KAAK,MAAM,EAAE,eAAe,OAAO,SAAS,SAAS,QAAQ,GAAG,KAAK;AAAA,MACtI;AAAA,IACF;AAAA,IAEA,MAAM,MAAM,OAAiC,OAA8E;AACzH,YAAM,QAAqC;AAAA,QACzC,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,MAClB;AAEA,UAAI,MAAM,cAAe,OAAM,gBAAgB,MAAM;AACrD,UAAI,MAAM,MAAO,OAAM,QAAQ,MAAM;AACrC,UAAI,MAAM,MAAO,OAAM,QAAQ,MAAM;AACrC,UAAI,MAAM,WAAY,OAAM,kBAAkB,MAAM;AACpD,UAAI,MAAM,SAAU,OAAM,gBAAgB,MAAM;AAEhD,YAAM,QAAQ,MAAM;AAAA,QAClB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,UACE,SAAS,EAAE,WAAW,OAAO;AAAA,UAC7B,OAAO,MAAM;AAAA,UACb,SAAS,MAAM,OAAO,KAAK,MAAM;AAAA,QACnC;AAAA,QACA;AAAA,MACF;AACA,YAAM,QAAQ,MAAM,GAAG,MAAM,gBAAgB,KAAK;AAClD,aAAO,EAAE,OAAO,MAAM;AAAA,IACxB;AAAA,IAEA,MAAM,eAAe,MAAc,OAA0C;AAC3E,YAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,KAAK,GAAI;AAClE,YAAM,eAAe,MAAM,GAAG,aAAa,gBAAgB;AAAA,QACzD,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,QAChB,WAAW,EAAE,KAAK,UAAU;AAAA,MAC9B,CAAC;AACD,aAAO;AAAA,IACT;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { IntegrationScope } from '@open-mercato/shared/modules/integrations/types'\nimport type { ListIntegrationLogsQuery } from '../data/validators'\nimport { IntegrationLog } from '../data/entities'\n\ntype LogInput = {\n integrationId: string\n runId?: string | null\n scopeEntityType?: string | null\n scopeEntityId?: string | null\n level: 'info' | 'warn' | 'error'\n message: string\n code?: string | null\n payload?: Record<string, unknown> | null\n}\n\nexport function createIntegrationLogService(em: EntityManager) {\n return {\n async write(input: LogInput, scope: IntegrationScope): Promise<IntegrationLog> {\n const row = em.create(IntegrationLog, {\n integrationId: input.integrationId,\n runId: input.runId,\n scopeEntityType: input.scopeEntityType,\n scopeEntityId: input.scopeEntityId,\n level: input.level,\n message: input.message,\n code: input.code,\n payload: input.payload,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n await em.persistAndFlush(row)\n return row\n },\n\n scoped(integrationId: string, scope: IntegrationScope) {\n return {\n info: (message: string, payload?: Record<string, unknown>) => this.write({ integrationId, level: 'info', message, payload }, scope),\n warn: (message: string, payload?: Record<string, unknown>) => this.write({ integrationId, level: 'warn', message, payload }, scope),\n error: (message: string, payload?: Record<string, unknown>) => this.write({ integrationId, level: 'error', message, payload }, scope),\n }\n },\n\n async query(query: ListIntegrationLogsQuery, scope: IntegrationScope): Promise<{ items: IntegrationLog[]; total: number }> {\n const where: FilterQuery<IntegrationLog> = {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n }\n\n if (query.integrationId) where.integrationId = query.integrationId\n if (query.level) where.level = query.level\n if (query.runId) where.runId = query.runId\n if (query.entityType) where.scopeEntityType = query.entityType\n if (query.entityId) where.scopeEntityId = query.entityId\n\n const items = await findWithDecryption(\n em,\n IntegrationLog,\n where,\n {\n orderBy: { createdAt: 'DESC' },\n limit: query.pageSize,\n offset: (query.page - 1) * query.pageSize,\n },\n scope,\n )\n const total = await em.count(IntegrationLog, where)\n return { items, total }\n },\n\n async pruneOlderThan(days: number, scope: IntegrationScope): Promise<number> {\n const threshold = new Date(Date.now() - days * 24 * 60 * 60 * 1000)\n const deletedCount = await em.nativeDelete(IntegrationLog, {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n createdAt: { $lt: threshold },\n })\n return deletedCount\n },\n }\n}\n\nexport type IntegrationLogService = ReturnType<typeof createIntegrationLogService>\n"],
5
+ "mappings": "AACA,SAAS,0BAA0B;AAGnC,SAAS,sBAAsB;AAaxB,SAAS,4BAA4B,IAAmB;AAC7D,SAAO;AAAA,IACL,MAAM,MAAM,OAAiB,OAAkD;AAC7E,YAAM,MAAM,GAAG,OAAO,gBAAgB;AAAA,QACpC,eAAe,MAAM;AAAA,QACrB,OAAO,MAAM;AAAA,QACb,iBAAiB,MAAM;AAAA,QACvB,eAAe,MAAM;AAAA,QACrB,OAAO,MAAM;AAAA,QACb,SAAS,MAAM;AAAA,QACf,MAAM,MAAM;AAAA,QACZ,SAAS,MAAM;AAAA,QACf,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,MAClB,CAAC;AACD,YAAM,GAAG,gBAAgB,GAAG;AAC5B,aAAO;AAAA,IACT;AAAA,IAEA,OAAO,eAAuB,OAAyB;AACrD,aAAO;AAAA,QACL,MAAM,CAAC,SAAiB,YAAsC,KAAK,MAAM,EAAE,eAAe,OAAO,QAAQ,SAAS,QAAQ,GAAG,KAAK;AAAA,QAClI,MAAM,CAAC,SAAiB,YAAsC,KAAK,MAAM,EAAE,eAAe,OAAO,QAAQ,SAAS,QAAQ,GAAG,KAAK;AAAA,QAClI,OAAO,CAAC,SAAiB,YAAsC,KAAK,MAAM,EAAE,eAAe,OAAO,SAAS,SAAS,QAAQ,GAAG,KAAK;AAAA,MACtI;AAAA,IACF;AAAA,IAEA,MAAM,MAAM,OAAiC,OAA8E;AACzH,YAAM,QAAqC;AAAA,QACzC,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,MAClB;AAEA,UAAI,MAAM,cAAe,OAAM,gBAAgB,MAAM;AACrD,UAAI,MAAM,MAAO,OAAM,QAAQ,MAAM;AACrC,UAAI,MAAM,MAAO,OAAM,QAAQ,MAAM;AACrC,UAAI,MAAM,WAAY,OAAM,kBAAkB,MAAM;AACpD,UAAI,MAAM,SAAU,OAAM,gBAAgB,MAAM;AAEhD,YAAM,QAAQ,MAAM;AAAA,QAClB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,UACE,SAAS,EAAE,WAAW,OAAO;AAAA,UAC7B,OAAO,MAAM;AAAA,UACb,SAAS,MAAM,OAAO,KAAK,MAAM;AAAA,QACnC;AAAA,QACA;AAAA,MACF;AACA,YAAM,QAAQ,MAAM,GAAG,MAAM,gBAAgB,KAAK;AAClD,aAAO,EAAE,OAAO,MAAM;AAAA,IACxB;AAAA,IAEA,MAAM,eAAe,MAAc,OAA0C;AAC3E,YAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,KAAK,GAAI;AAClE,YAAM,eAAe,MAAM,GAAG,aAAa,gBAAgB;AAAA,QACzD,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,QAChB,WAAW,EAAE,KAAK,UAAU;AAAA,MAC9B,CAAC;AACD,aAAO;AAAA,IACT;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/integrations/lib/state-service.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { IntegrationState } from '../data/entities'\nimport type { IntegrationScope } from './types'\n\nexport function createIntegrationStateService(em: EntityManager) {\n return {\n async get(integrationId: string, scope: IntegrationScope): Promise<IntegrationState | null> {\n return findOneWithDecryption(\n em,\n IntegrationState,\n {\n integrationId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n },\n\n async upsert(\n integrationId: string,\n input: Partial<Pick<IntegrationState, 'isEnabled' | 'apiVersion' | 'reauthRequired' | 'lastHealthStatus' | 'lastHealthCheckedAt'>>,\n scope: IntegrationScope,\n ): Promise<IntegrationState> {\n const current = await this.get(integrationId, scope)\n if (current) {\n if (input.isEnabled !== undefined) current.isEnabled = input.isEnabled\n if (input.apiVersion !== undefined) current.apiVersion = input.apiVersion\n if (input.reauthRequired !== undefined) current.reauthRequired = input.reauthRequired\n if (input.lastHealthStatus !== undefined) current.lastHealthStatus = input.lastHealthStatus\n if (input.lastHealthCheckedAt !== undefined) current.lastHealthCheckedAt = input.lastHealthCheckedAt\n await em.flush()\n return current\n }\n\n const created = em.create(IntegrationState, {\n integrationId,\n isEnabled: input.isEnabled ?? true,\n apiVersion: input.apiVersion,\n reauthRequired: input.reauthRequired ?? false,\n lastHealthStatus: input.lastHealthStatus,\n lastHealthCheckedAt: input.lastHealthCheckedAt,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n await em.persistAndFlush(created)\n return created\n },\n\n async resolveApiVersion(integrationId: string, scope: IntegrationScope): Promise<string | undefined> {\n const state = await this.get(integrationId, scope)\n return state?.apiVersion ?? undefined\n },\n\n async setReauthRequired(integrationId: string, required: boolean, scope: IntegrationScope): Promise<IntegrationState> {\n return this.upsert(integrationId, { reauthRequired: required }, scope)\n },\n }\n}\n\nexport type IntegrationStateService = ReturnType<typeof createIntegrationStateService>\n"],
5
- "mappings": "AACA,SAAS,6BAA6B;AACtC,SAAS,wBAAwB;AAG1B,SAAS,8BAA8B,IAAmB;AAC/D,SAAO;AAAA,IACL,MAAM,IAAI,eAAuB,OAA2D;AAC1F,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,UACE;AAAA,UACA,gBAAgB,MAAM;AAAA,UACtB,UAAU,MAAM;AAAA,UAChB,WAAW;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,IAEA,MAAM,OACJ,eACA,OACA,OAC2B;AAC3B,YAAM,UAAU,MAAM,KAAK,IAAI,eAAe,KAAK;AACnD,UAAI,SAAS;AACX,YAAI,MAAM,cAAc,OAAW,SAAQ,YAAY,MAAM;AAC7D,YAAI,MAAM,eAAe,OAAW,SAAQ,aAAa,MAAM;AAC/D,YAAI,MAAM,mBAAmB,OAAW,SAAQ,iBAAiB,MAAM;AACvE,YAAI,MAAM,qBAAqB,OAAW,SAAQ,mBAAmB,MAAM;AAC3E,YAAI,MAAM,wBAAwB,OAAW,SAAQ,sBAAsB,MAAM;AACjF,cAAM,GAAG,MAAM;AACf,eAAO;AAAA,MACT;AAEA,YAAM,UAAU,GAAG,OAAO,kBAAkB;AAAA,QAC1C;AAAA,QACA,WAAW,MAAM,aAAa;AAAA,QAC9B,YAAY,MAAM;AAAA,QAClB,gBAAgB,MAAM,kBAAkB;AAAA,QACxC,kBAAkB,MAAM;AAAA,QACxB,qBAAqB,MAAM;AAAA,QAC3B,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,MAClB,CAAC;AACD,YAAM,GAAG,gBAAgB,OAAO;AAChC,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,kBAAkB,eAAuB,OAAsD;AACnG,YAAM,QAAQ,MAAM,KAAK,IAAI,eAAe,KAAK;AACjD,aAAO,OAAO,cAAc;AAAA,IAC9B;AAAA,IAEA,MAAM,kBAAkB,eAAuB,UAAmB,OAAoD;AACpH,aAAO,KAAK,OAAO,eAAe,EAAE,gBAAgB,SAAS,GAAG,KAAK;AAAA,IACvE;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { IntegrationScope } from '@open-mercato/shared/modules/integrations/types'\nimport { IntegrationState } from '../data/entities'\n\nexport function createIntegrationStateService(em: EntityManager) {\n return {\n async get(integrationId: string, scope: IntegrationScope): Promise<IntegrationState | null> {\n return findOneWithDecryption(\n em,\n IntegrationState,\n {\n integrationId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n },\n\n async upsert(\n integrationId: string,\n input: Partial<Pick<IntegrationState, 'isEnabled' | 'apiVersion' | 'reauthRequired' | 'lastHealthStatus' | 'lastHealthCheckedAt'>>,\n scope: IntegrationScope,\n ): Promise<IntegrationState> {\n const current = await this.get(integrationId, scope)\n if (current) {\n if (input.isEnabled !== undefined) current.isEnabled = input.isEnabled\n if (input.apiVersion !== undefined) current.apiVersion = input.apiVersion\n if (input.reauthRequired !== undefined) current.reauthRequired = input.reauthRequired\n if (input.lastHealthStatus !== undefined) current.lastHealthStatus = input.lastHealthStatus\n if (input.lastHealthCheckedAt !== undefined) current.lastHealthCheckedAt = input.lastHealthCheckedAt\n await em.flush()\n return current\n }\n\n const created = em.create(IntegrationState, {\n integrationId,\n isEnabled: input.isEnabled ?? true,\n apiVersion: input.apiVersion,\n reauthRequired: input.reauthRequired ?? false,\n lastHealthStatus: input.lastHealthStatus,\n lastHealthCheckedAt: input.lastHealthCheckedAt,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n await em.persistAndFlush(created)\n return created\n },\n\n async resolveApiVersion(integrationId: string, scope: IntegrationScope): Promise<string | undefined> {\n const state = await this.get(integrationId, scope)\n return state?.apiVersion ?? undefined\n },\n\n async setReauthRequired(integrationId: string, required: boolean, scope: IntegrationScope): Promise<IntegrationState> {\n return this.upsert(integrationId, { reauthRequired: required }, scope)\n },\n }\n}\n\nexport type IntegrationStateService = ReturnType<typeof createIntegrationStateService>\n"],
5
+ "mappings": "AACA,SAAS,6BAA6B;AAEtC,SAAS,wBAAwB;AAE1B,SAAS,8BAA8B,IAAmB;AAC/D,SAAO;AAAA,IACL,MAAM,IAAI,eAAuB,OAA2D;AAC1F,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,UACE;AAAA,UACA,gBAAgB,MAAM;AAAA,UACtB,UAAU,MAAM;AAAA,UAChB,WAAW;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,IAEA,MAAM,OACJ,eACA,OACA,OAC2B;AAC3B,YAAM,UAAU,MAAM,KAAK,IAAI,eAAe,KAAK;AACnD,UAAI,SAAS;AACX,YAAI,MAAM,cAAc,OAAW,SAAQ,YAAY,MAAM;AAC7D,YAAI,MAAM,eAAe,OAAW,SAAQ,aAAa,MAAM;AAC/D,YAAI,MAAM,mBAAmB,OAAW,SAAQ,iBAAiB,MAAM;AACvE,YAAI,MAAM,qBAAqB,OAAW,SAAQ,mBAAmB,MAAM;AAC3E,YAAI,MAAM,wBAAwB,OAAW,SAAQ,sBAAsB,MAAM;AACjF,cAAM,GAAG,MAAM;AACf,eAAO;AAAA,MACT;AAEA,YAAM,UAAU,GAAG,OAAO,kBAAkB;AAAA,QAC1C;AAAA,QACA,WAAW,MAAM,aAAa;AAAA,QAC9B,YAAY,MAAM;AAAA,QAClB,gBAAgB,MAAM,kBAAkB;AAAA,QACxC,kBAAkB,MAAM;AAAA,QACxB,qBAAqB,MAAM;AAAA,QAC3B,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,MAClB,CAAC;AACD,YAAM,GAAG,gBAAgB,OAAO;AAChC,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,kBAAkB,eAAuB,OAAsD;AACnG,YAAM,QAAQ,MAAM,KAAK,IAAI,eAAe,KAAK;AACjD,aAAO,OAAO,cAAc;AAAA,IAC9B;AAAA,IAEA,MAAM,kBAAkB,eAAuB,UAAmB,OAAoD;AACpH,aAAO,KAAK,OAAO,eAAe,EAAE,gBAAgB,SAAS,GAAG,KAAK;AAAA,IACvE;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -7,9 +7,9 @@ import { serializeOperationMetadata } from "@open-mercato/shared/lib/commands/op
7
7
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
8
8
  import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
9
9
  import {
10
- runCrudMutationGuardAfterSuccess,
11
- validateCrudMutationGuard
12
- } from "@open-mercato/shared/lib/crud/mutation-guard";
10
+ bridgeLegacyGuard,
11
+ runMutationGuards
12
+ } from "@open-mercato/shared/lib/crud/mutation-guard-registry";
13
13
  import { withScopedPayload } from "../../utils.js";
14
14
  const convertSchema = z.object({
15
15
  quoteId: z.string().uuid(),
@@ -19,9 +19,28 @@ const convertSchema = z.object({
19
19
  const metadata = {
20
20
  POST: { requireAuth: true, requireFeatures: ["sales.quotes.manage", "sales.orders.manage"] }
21
21
  };
22
- function buildMutationGuardErrorResponse(validation) {
23
- if (validation.ok) return null;
24
- return NextResponse.json(validation.body, { status: validation.status });
22
+ function resolveUserFeatures(auth) {
23
+ const features = auth?.features;
24
+ if (!Array.isArray(features)) return [];
25
+ return features.filter((value) => typeof value === "string");
26
+ }
27
+ async function runGuards(ctx, input) {
28
+ const legacyGuard = bridgeLegacyGuard(ctx.container);
29
+ if (!legacyGuard) {
30
+ return { ok: true, afterSuccessCallbacks: [] };
31
+ }
32
+ return runMutationGuards([legacyGuard], input, {
33
+ userFeatures: resolveUserFeatures(ctx.auth)
34
+ });
35
+ }
36
+ async function runGuardAfterSuccessCallbacks(callbacks, input) {
37
+ for (const callback of callbacks) {
38
+ if (!callback.guard.afterSuccess) continue;
39
+ await callback.guard.afterSuccess({
40
+ ...input,
41
+ metadata: callback.metadata ?? null
42
+ });
43
+ }
25
44
  }
26
45
  async function resolveRequestContext(req) {
27
46
  const container = await createRequestContainer();
@@ -54,7 +73,7 @@ async function POST(req) {
54
73
  const payload = await req.json().catch(() => ({}));
55
74
  const scoped = withScopedPayload(payload ?? {}, ctx, translate);
56
75
  const input = convertSchema.parse(scoped);
57
- const mutationGuardValidation = await validateCrudMutationGuard(ctx.container, {
76
+ const guardResult = await runGuards(ctx, {
58
77
  tenantId: ctx.auth?.tenantId ?? "",
59
78
  organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
60
79
  userId: ctx.auth?.sub ?? "",
@@ -64,9 +83,8 @@ async function POST(req) {
64
83
  requestMethod: req.method,
65
84
  requestHeaders: req.headers
66
85
  });
67
- if (mutationGuardValidation) {
68
- const lockErrorResponse = buildMutationGuardErrorResponse(mutationGuardValidation);
69
- if (lockErrorResponse) return lockErrorResponse;
86
+ if (!guardResult.ok) {
87
+ return NextResponse.json(guardResult.errorBody ?? { error: "Operation blocked by guard" }, { status: guardResult.errorStatus ?? 422 });
70
88
  }
71
89
  const commandBus = ctx.container.resolve("commandBus");
72
90
  const { result, logEntry } = await commandBus.execute("sales.quotes.convert_to_order", { input, ctx });
@@ -86,8 +104,8 @@ async function POST(req) {
86
104
  })
87
105
  );
88
106
  }
89
- if (mutationGuardValidation?.ok && mutationGuardValidation.shouldRunAfterSuccess) {
90
- await runCrudMutationGuardAfterSuccess(ctx.container, {
107
+ if (guardResult.afterSuccessCallbacks.length) {
108
+ await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, {
91
109
  tenantId: ctx.auth?.tenantId ?? "",
92
110
  organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
93
111
  userId: ctx.auth?.sub ?? "",
@@ -95,8 +113,7 @@ async function POST(req) {
95
113
  resourceId: input.quoteId,
96
114
  operation: "update",
97
115
  requestMethod: req.method,
98
- requestHeaders: req.headers,
99
- metadata: mutationGuardValidation.metadata ?? null
116
+ requestHeaders: req.headers
100
117
  });
101
118
  }
102
119
  return jsonResponse;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/sales/api/quotes/convert/route.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 { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n type CrudMutationGuardValidationResult,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { withScopedPayload } from '../../utils'\n\nconst convertSchema = z.object({\n quoteId: z.string().uuid(),\n orderId: z.string().uuid().optional(),\n orderNumber: z.string().trim().max(191).optional(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['sales.quotes.manage', 'sales.orders.manage'] },\n}\n\ntype RequestContext = {\n ctx: CommandRuntimeContext\n}\n\nfunction buildMutationGuardErrorResponse(validation: CrudMutationGuardValidationResult): NextResponse | null {\n if (validation.ok) return null\n return NextResponse.json(validation.body, { status: validation.status })\n}\n\nasync function resolveRequestContext(req: Request): Promise<RequestContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('sales.documents.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('sales.documents.errors.organization_required', 'Organization context is required'),\n })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return { ctx }\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx } = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(payload ?? {}, ctx, translate)\n const input = convertSchema.parse(scoped)\n const mutationGuardValidation = await validateCrudMutationGuard(ctx.container, {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n resourceKind: 'sales.quote',\n resourceId: input.quoteId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n if (mutationGuardValidation) {\n const lockErrorResponse = buildMutationGuardErrorResponse(mutationGuardValidation)\n if (lockErrorResponse) return lockErrorResponse\n }\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const { result, logEntry } = await commandBus.execute<\n { quoteId: string; orderId?: string; orderNumber?: string },\n { orderId: string }\n >('sales.quotes.convert_to_order', { input, ctx })\n\n const orderId = result?.orderId ?? input.orderId ?? input.quoteId\n const jsonResponse = NextResponse.json({ orderId })\n\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n jsonResponse.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'sales.order',\n resourceId: logEntry.resourceId ?? orderId,\n executedAt: logEntry.createdAt instanceof Date\n ? logEntry.createdAt.toISOString()\n : typeof logEntry.createdAt === 'string'\n ? logEntry.createdAt\n : new Date().toISOString(),\n })\n )\n }\n\n if (mutationGuardValidation?.ok && mutationGuardValidation.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(ctx.container, {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n resourceKind: 'sales.quote',\n resourceId: input.quoteId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: mutationGuardValidation.metadata ?? null,\n })\n }\n\n return jsonResponse\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('sales.quotes.convert failed', err)\n return NextResponse.json(\n { error: translate('sales.documents.detail.convertError', 'Failed to convert quote.') },\n { status: 400 }\n )\n }\n}\n\nconst convertResponseSchema = z.object({\n orderId: z.string().uuid(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Sales',\n summary: 'Convert quote to order',\n methods: {\n POST: {\n summary: 'Convert quote',\n description: 'Creates a sales order from a quote and removes the original quote record.',\n requestBody: {\n contentType: 'application/json',\n schema: convertSchema,\n },\n responses: [\n { status: 200, description: 'Conversion succeeded', schema: convertResponseSchema },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n { status: 409, description: 'Conflict detected', schema: z.object({ error: z.string(), code: z.string().optional() }) },\n { status: 423, description: 'Record locked', schema: z.object({ error: z.string(), code: z.string().optional() }) },\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 { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n type MutationGuardInput,\n} from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport { withScopedPayload } from '../../utils'\n\nconst convertSchema = z.object({\n quoteId: z.string().uuid(),\n orderId: z.string().uuid().optional(),\n orderNumber: z.string().trim().max(191).optional(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['sales.quotes.manage', 'sales.orders.manage'] },\n}\n\ntype RequestContext = {\n ctx: CommandRuntimeContext\n}\n\nfunction resolveUserFeatures(auth: unknown): string[] {\n const features = (auth as { features?: unknown })?.features\n if (!Array.isArray(features)) return []\n return features.filter((value): value is string => typeof value === 'string')\n}\n\nasync function runGuards(\n ctx: CommandRuntimeContext,\n input: MutationGuardInput,\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>\n}> {\n const legacyGuard = bridgeLegacyGuard(ctx.container)\n if (!legacyGuard) {\n return { ok: true, afterSuccessCallbacks: [] }\n }\n\n return runMutationGuards([legacyGuard], input, {\n userFeatures: resolveUserFeatures(ctx.auth),\n })\n}\n\nasync function runGuardAfterSuccessCallbacks(\n callbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>,\n input: {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n },\n): Promise<void> {\n for (const callback of callbacks) {\n if (!callback.guard.afterSuccess) continue\n await callback.guard.afterSuccess({\n ...input,\n metadata: callback.metadata ?? null,\n })\n }\n}\n\nasync function resolveRequestContext(req: Request): Promise<RequestContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('sales.documents.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('sales.documents.errors.organization_required', 'Organization context is required'),\n })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return { ctx }\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx } = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(payload ?? {}, ctx, translate)\n const input = convertSchema.parse(scoped)\n const guardResult = await runGuards(ctx, {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n resourceKind: 'sales.quote',\n resourceId: input.quoteId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n if (!guardResult.ok) {\n return NextResponse.json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })\n }\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const { result, logEntry } = await commandBus.execute<\n { quoteId: string; orderId?: string; orderNumber?: string },\n { orderId: string }\n >('sales.quotes.convert_to_order', { input, ctx })\n\n const orderId = result?.orderId ?? input.orderId ?? input.quoteId\n const jsonResponse = NextResponse.json({ orderId })\n\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n jsonResponse.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'sales.order',\n resourceId: logEntry.resourceId ?? orderId,\n executedAt: logEntry.createdAt instanceof Date\n ? logEntry.createdAt.toISOString()\n : typeof logEntry.createdAt === 'string'\n ? logEntry.createdAt\n : new Date().toISOString(),\n })\n )\n }\n\n if (guardResult.afterSuccessCallbacks.length) {\n await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n resourceKind: 'sales.quote',\n resourceId: input.quoteId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n }\n\n return jsonResponse\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('sales.quotes.convert failed', err)\n return NextResponse.json(\n { error: translate('sales.documents.detail.convertError', 'Failed to convert quote.') },\n { status: 400 }\n )\n }\n}\n\nconst convertResponseSchema = z.object({\n orderId: z.string().uuid(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Sales',\n summary: 'Convert quote to order',\n methods: {\n POST: {\n summary: 'Convert quote',\n description: 'Creates a sales order from a quote and removes the original quote record.',\n requestBody: {\n contentType: 'application/json',\n schema: convertSchema,\n },\n responses: [\n { status: 200, description: 'Conversion succeeded', schema: convertResponseSchema },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n { status: 409, description: 'Conflict detected', schema: z.object({ error: z.string(), code: z.string().optional() }) },\n { status: 423, description: 'Record locked', schema: z.object({ error: z.string(), code: z.string().optional() }) },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -6,9 +6,9 @@ import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/d
6
6
  import { resolveTranslations, detectLocale } from "@open-mercato/shared/lib/i18n/server";
7
7
  import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
8
8
  import {
9
- runCrudMutationGuardAfterSuccess,
10
- validateCrudMutationGuard
11
- } from "@open-mercato/shared/lib/crud/mutation-guard";
9
+ bridgeLegacyGuard,
10
+ runMutationGuards
11
+ } from "@open-mercato/shared/lib/crud/mutation-guard-registry";
12
12
  import crypto from "node:crypto";
13
13
  import { withScopedPayload } from "../../utils.js";
14
14
  import { SalesQuote } from "../../../data/entities.js";
@@ -19,9 +19,28 @@ import { QuoteSentEmail } from "../../../emails/QuoteSentEmail.js";
19
19
  const metadata = {
20
20
  POST: { requireAuth: true, requireFeatures: ["sales.quotes.manage"] }
21
21
  };
22
- function buildMutationGuardErrorResponse(validation) {
23
- if (validation.ok) return null;
24
- return NextResponse.json(validation.body, { status: validation.status });
22
+ function resolveUserFeatures(auth) {
23
+ const features = auth?.features;
24
+ if (!Array.isArray(features)) return [];
25
+ return features.filter((value) => typeof value === "string");
26
+ }
27
+ async function runGuards(ctx, input) {
28
+ const legacyGuard = bridgeLegacyGuard(ctx.container);
29
+ if (!legacyGuard) {
30
+ return { ok: true, afterSuccessCallbacks: [] };
31
+ }
32
+ return runMutationGuards([legacyGuard], input, {
33
+ userFeatures: resolveUserFeatures(ctx.auth)
34
+ });
35
+ }
36
+ async function runGuardAfterSuccessCallbacks(callbacks, input) {
37
+ for (const callback of callbacks) {
38
+ if (!callback.guard.afterSuccess) continue;
39
+ await callback.guard.afterSuccess({
40
+ ...input,
41
+ metadata: callback.metadata ?? null
42
+ });
43
+ }
25
44
  }
26
45
  async function resolveRequestContext(req) {
27
46
  const container = await createRequestContainer();
@@ -64,7 +83,7 @@ async function POST(req) {
64
83
  const payload = await req.json().catch(() => ({}));
65
84
  const scoped = withScopedPayload(payload ?? {}, ctx, translate);
66
85
  const input = quoteSendSchema.parse(scoped);
67
- const mutationGuardValidation = await validateCrudMutationGuard(ctx.container, {
86
+ const guardResult = await runGuards(ctx, {
68
87
  tenantId: ctx.auth?.tenantId ?? "",
69
88
  organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
70
89
  userId: ctx.auth?.sub ?? "",
@@ -74,9 +93,8 @@ async function POST(req) {
74
93
  requestMethod: req.method,
75
94
  requestHeaders: req.headers
76
95
  });
77
- if (mutationGuardValidation) {
78
- const lockErrorResponse = buildMutationGuardErrorResponse(mutationGuardValidation);
79
- if (lockErrorResponse) return lockErrorResponse;
96
+ if (!guardResult.ok) {
97
+ return NextResponse.json(guardResult.errorBody ?? { error: "Operation blocked by guard" }, { status: guardResult.errorStatus ?? 422 });
80
98
  }
81
99
  const em = ctx.container.resolve("em").fork();
82
100
  const quote = await em.findOne(SalesQuote, { id: input.quoteId, deletedAt: null });
@@ -132,8 +150,8 @@ async function POST(req) {
132
150
  subject: translate("sales.quotes.email.subject", "Quote {quoteNumber}", { quoteNumber: quote.quoteNumber }),
133
151
  react: QuoteSentEmail({ url, copy })
134
152
  });
135
- if (mutationGuardValidation?.ok && mutationGuardValidation.shouldRunAfterSuccess) {
136
- await runCrudMutationGuardAfterSuccess(ctx.container, {
153
+ if (guardResult.afterSuccessCallbacks.length) {
154
+ await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, {
137
155
  tenantId: ctx.auth?.tenantId ?? "",
138
156
  organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
139
157
  userId: ctx.auth?.sub ?? "",
@@ -141,8 +159,7 @@ async function POST(req) {
141
159
  resourceId: input.quoteId,
142
160
  operation: "update",
143
161
  requestMethod: req.method,
144
- requestHeaders: req.headers,
145
- metadata: mutationGuardValidation.metadata ?? null
162
+ requestHeaders: req.headers
146
163
  });
147
164
  }
148
165
  return NextResponse.json({ ok: true });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/sales/api/quotes/send/route.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 { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations, detectLocale } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n type CrudMutationGuardValidationResult,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport crypto from 'node:crypto'\nimport { withScopedPayload } from '../../utils'\nimport { SalesQuote } from '../../../data/entities'\nimport { quoteSendSchema } from '../../../data/validators'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport { resolveStatusEntryIdByValue } from '../../../lib/statusHelpers'\nimport { QuoteSentEmail } from '../../../emails/QuoteSentEmail'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['sales.quotes.manage'] },\n}\n\ntype RequestContext = {\n ctx: CommandRuntimeContext\n}\n\nfunction buildMutationGuardErrorResponse(validation: CrudMutationGuardValidationResult): NextResponse | null {\n if (validation.ok) return null\n return NextResponse.json(validation.body, { status: validation.status })\n}\n\nasync function resolveRequestContext(req: Request): Promise<RequestContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('sales.documents.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('sales.documents.errors.organization_required', 'Organization context is required'),\n })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return { ctx }\n}\n\nfunction resolveQuoteEmail(quote: SalesQuote): string | null {\n const snapshot = quote.customerSnapshot && typeof quote.customerSnapshot === 'object' ? (quote.customerSnapshot as Record<string, unknown>) : null\n const metadata = quote.metadata && typeof quote.metadata === 'object' ? (quote.metadata as Record<string, unknown>) : null\n const contact = snapshot?.contact as Record<string, unknown> | undefined\n const customer = snapshot?.customer as Record<string, unknown> | undefined\n const candidate =\n (typeof contact?.email === 'string' && contact.email.trim()) ||\n (typeof customer?.primaryEmail === 'string' && customer.primaryEmail.trim()) ||\n (typeof metadata?.customerEmail === 'string' && metadata.customerEmail.trim()) ||\n null\n if (!candidate) return null\n const parsed = z.string().email().safeParse(candidate)\n return parsed.success ? parsed.data : null\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx } = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(payload ?? {}, ctx, translate)\n const input = quoteSendSchema.parse(scoped)\n const mutationGuardValidation = await validateCrudMutationGuard(ctx.container, {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n resourceKind: 'sales.quote',\n resourceId: input.quoteId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n if (mutationGuardValidation) {\n const lockErrorResponse = buildMutationGuardErrorResponse(mutationGuardValidation)\n if (lockErrorResponse) return lockErrorResponse\n }\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const quote = await em.findOne(SalesQuote, { id: input.quoteId, deletedAt: null })\n if (!quote) {\n throw new CrudHttpError(404, { error: translate('sales.documents.detail.error', 'Document not found or inaccessible.') })\n }\n if (quote.tenantId !== ctx.auth?.tenantId || quote.organizationId !== ctx.selectedOrganizationId) {\n throw new CrudHttpError(403, { error: translate('sales.documents.errors.forbidden', 'Forbidden') })\n }\n\n if ((quote.status ?? null) === 'canceled') {\n throw new CrudHttpError(400, { error: translate('sales.quotes.send.canceled', 'Canceled quotes cannot be sent.') })\n }\n\n const email = resolveQuoteEmail(quote)\n if (!email) {\n throw new CrudHttpError(400, { error: translate('sales.quotes.send.missingEmail', 'Customer email is required to send a quote.') })\n }\n\n const now = new Date()\n const validUntil = new Date(now)\n validUntil.setUTCDate(validUntil.getUTCDate() + input.validForDays)\n\n quote.validUntil = validUntil\n quote.acceptanceToken = crypto.randomUUID()\n quote.sentAt = now\n quote.status = 'sent'\n quote.statusEntryId = await resolveStatusEntryIdByValue(em, {\n tenantId: quote.tenantId,\n organizationId: quote.organizationId,\n value: 'sent',\n })\n quote.updatedAt = now\n em.persist(quote)\n await em.flush()\n\n const appUrl = process.env.APP_URL || ''\n const url = appUrl ? `${appUrl.replace(/\\/$/, '')}/quote/${quote.acceptanceToken}` : `/quote/${quote.acceptanceToken}`\n\n const locale = await detectLocale()\n const validUntilFormatted = validUntil.toLocaleDateString(locale, {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n })\n\n const copy = {\n preview: translate('sales.quotes.email.preview', 'Quote {quoteNumber} is ready for review', { quoteNumber: quote.quoteNumber }),\n heading: translate('sales.quotes.email.heading', 'Quote {quoteNumber}', { quoteNumber: quote.quoteNumber }),\n total: translate('sales.quotes.email.total', 'Total: {amount} {currency}', {\n amount: quote.grandTotalGrossAmount ?? quote.grandTotalNetAmount ?? '0',\n currency: quote.currencyCode,\n }),\n validUntil: translate('sales.quotes.email.validUntil', 'Valid until: {date}', { date: validUntilFormatted }),\n cta: translate('sales.quotes.email.cta', 'View quote'),\n footer: translate('sales.quotes.email.footer', 'Open Mercato'),\n }\n\n await sendEmail({\n to: email,\n subject: translate('sales.quotes.email.subject', 'Quote {quoteNumber}', { quoteNumber: quote.quoteNumber }),\n react: QuoteSentEmail({ url, copy }),\n })\n\n if (mutationGuardValidation?.ok && mutationGuardValidation.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(ctx.container, {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n resourceKind: 'sales.quote',\n resourceId: input.quoteId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: mutationGuardValidation.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true })\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('sales.quotes.send failed', err)\n return NextResponse.json(\n { error: translate('sales.quotes.send.failed', 'Failed to send quote.') },\n { status: 400 }\n )\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Sales',\n summary: 'Send quote to customer',\n methods: {\n POST: {\n summary: 'Send quote',\n requestBody: {\n contentType: 'application/json',\n schema: quoteSendSchema,\n },\n responses: [\n { status: 200, description: 'Email queued', schema: z.object({ ok: z.literal(true) }) },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n { status: 404, description: 'Not found', schema: z.object({ error: z.string() }) },\n { status: 409, description: 'Conflict detected', schema: z.object({ error: z.string(), code: z.string().optional() }) },\n { status: 423, description: 'Record locked', schema: z.object({ error: z.string(), code: z.string().optional() }) },\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 { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations, detectLocale } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n type MutationGuardInput,\n} from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport crypto from 'node:crypto'\nimport { withScopedPayload } from '../../utils'\nimport { SalesQuote } from '../../../data/entities'\nimport { quoteSendSchema } from '../../../data/validators'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport { resolveStatusEntryIdByValue } from '../../../lib/statusHelpers'\nimport { QuoteSentEmail } from '../../../emails/QuoteSentEmail'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['sales.quotes.manage'] },\n}\n\ntype RequestContext = {\n ctx: CommandRuntimeContext\n}\n\nfunction resolveUserFeatures(auth: unknown): string[] {\n const features = (auth as { features?: unknown })?.features\n if (!Array.isArray(features)) return []\n return features.filter((value): value is string => typeof value === 'string')\n}\n\nasync function runGuards(\n ctx: CommandRuntimeContext,\n input: MutationGuardInput,\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>\n}> {\n const legacyGuard = bridgeLegacyGuard(ctx.container)\n if (!legacyGuard) {\n return { ok: true, afterSuccessCallbacks: [] }\n }\n\n return runMutationGuards([legacyGuard], input, {\n userFeatures: resolveUserFeatures(ctx.auth),\n })\n}\n\nasync function runGuardAfterSuccessCallbacks(\n callbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>,\n input: {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n },\n): Promise<void> {\n for (const callback of callbacks) {\n if (!callback.guard.afterSuccess) continue\n await callback.guard.afterSuccess({\n ...input,\n metadata: callback.metadata ?? null,\n })\n }\n}\n\nasync function resolveRequestContext(req: Request): Promise<RequestContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('sales.documents.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('sales.documents.errors.organization_required', 'Organization context is required'),\n })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return { ctx }\n}\n\nfunction resolveQuoteEmail(quote: SalesQuote): string | null {\n const snapshot = quote.customerSnapshot && typeof quote.customerSnapshot === 'object' ? (quote.customerSnapshot as Record<string, unknown>) : null\n const metadata = quote.metadata && typeof quote.metadata === 'object' ? (quote.metadata as Record<string, unknown>) : null\n const contact = snapshot?.contact as Record<string, unknown> | undefined\n const customer = snapshot?.customer as Record<string, unknown> | undefined\n const candidate =\n (typeof contact?.email === 'string' && contact.email.trim()) ||\n (typeof customer?.primaryEmail === 'string' && customer.primaryEmail.trim()) ||\n (typeof metadata?.customerEmail === 'string' && metadata.customerEmail.trim()) ||\n null\n if (!candidate) return null\n const parsed = z.string().email().safeParse(candidate)\n return parsed.success ? parsed.data : null\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx } = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(payload ?? {}, ctx, translate)\n const input = quoteSendSchema.parse(scoped)\n const guardResult = await runGuards(ctx, {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n resourceKind: 'sales.quote',\n resourceId: input.quoteId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n if (!guardResult.ok) {\n return NextResponse.json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })\n }\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const quote = await em.findOne(SalesQuote, { id: input.quoteId, deletedAt: null })\n if (!quote) {\n throw new CrudHttpError(404, { error: translate('sales.documents.detail.error', 'Document not found or inaccessible.') })\n }\n if (quote.tenantId !== ctx.auth?.tenantId || quote.organizationId !== ctx.selectedOrganizationId) {\n throw new CrudHttpError(403, { error: translate('sales.documents.errors.forbidden', 'Forbidden') })\n }\n\n if ((quote.status ?? null) === 'canceled') {\n throw new CrudHttpError(400, { error: translate('sales.quotes.send.canceled', 'Canceled quotes cannot be sent.') })\n }\n\n const email = resolveQuoteEmail(quote)\n if (!email) {\n throw new CrudHttpError(400, { error: translate('sales.quotes.send.missingEmail', 'Customer email is required to send a quote.') })\n }\n\n const now = new Date()\n const validUntil = new Date(now)\n validUntil.setUTCDate(validUntil.getUTCDate() + input.validForDays)\n\n quote.validUntil = validUntil\n quote.acceptanceToken = crypto.randomUUID()\n quote.sentAt = now\n quote.status = 'sent'\n quote.statusEntryId = await resolveStatusEntryIdByValue(em, {\n tenantId: quote.tenantId,\n organizationId: quote.organizationId,\n value: 'sent',\n })\n quote.updatedAt = now\n em.persist(quote)\n await em.flush()\n\n const appUrl = process.env.APP_URL || ''\n const url = appUrl ? `${appUrl.replace(/\\/$/, '')}/quote/${quote.acceptanceToken}` : `/quote/${quote.acceptanceToken}`\n\n const locale = await detectLocale()\n const validUntilFormatted = validUntil.toLocaleDateString(locale, {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n })\n\n const copy = {\n preview: translate('sales.quotes.email.preview', 'Quote {quoteNumber} is ready for review', { quoteNumber: quote.quoteNumber }),\n heading: translate('sales.quotes.email.heading', 'Quote {quoteNumber}', { quoteNumber: quote.quoteNumber }),\n total: translate('sales.quotes.email.total', 'Total: {amount} {currency}', {\n amount: quote.grandTotalGrossAmount ?? quote.grandTotalNetAmount ?? '0',\n currency: quote.currencyCode,\n }),\n validUntil: translate('sales.quotes.email.validUntil', 'Valid until: {date}', { date: validUntilFormatted }),\n cta: translate('sales.quotes.email.cta', 'View quote'),\n footer: translate('sales.quotes.email.footer', 'Open Mercato'),\n }\n\n await sendEmail({\n to: email,\n subject: translate('sales.quotes.email.subject', 'Quote {quoteNumber}', { quoteNumber: quote.quoteNumber }),\n react: QuoteSentEmail({ url, copy }),\n })\n\n if (guardResult.afterSuccessCallbacks.length) {\n await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n resourceKind: 'sales.quote',\n resourceId: input.quoteId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n }\n\n return NextResponse.json({ ok: true })\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('sales.quotes.send failed', err)\n return NextResponse.json(\n { error: translate('sales.quotes.send.failed', 'Failed to send quote.') },\n { status: 400 }\n )\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Sales',\n summary: 'Send quote to customer',\n methods: {\n POST: {\n summary: 'Send quote',\n requestBody: {\n contentType: 'application/json',\n schema: quoteSendSchema,\n },\n responses: [\n { status: 200, description: 'Email queued', schema: z.object({ ok: z.literal(true) }) },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n { status: 404, description: 'Not found', schema: z.object({ error: z.string() }) },\n { status: 409, description: 'Conflict detected', schema: z.object({ error: z.string(), code: z.string().optional() }) },\n { status: 423, description: 'Record locked', schema: z.object({ error: z.string(), code: z.string().optional() }) },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AAEnD,SAAS,qBAAqB,oBAAoB;AAClD,SAAS,qBAAqB;AAE9B;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AAEP,OAAO,YAAY;AACnB,SAAS,yBAAyB;AAClC,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB;AAChC,SAAS,iBAAiB;AAC1B,SAAS,mCAAmC;AAC5C,SAAS,sBAAsB;AAExB,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,qBAAqB,EAAE;AACtE;AAMA,SAAS,oBAAoB,MAAyB;AACpD,QAAM,WAAY,MAAiC;AACnD,MAAI,CAAC,MAAM,QAAQ,QAAQ,EAAG,QAAO,CAAC;AACtC,SAAO,SAAS,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ;AAC9E;AAEA,eAAe,UACb,KACA,OAMC;AACD,QAAM,cAAc,kBAAkB,IAAI,SAAS;AACnD,MAAI,CAAC,aAAa;AAChB,WAAO,EAAE,IAAI,MAAM,uBAAuB,CAAC,EAAE;AAAA,EAC/C;AAEA,SAAO,kBAAkB,CAAC,WAAW,GAAG,OAAO;AAAA,IAC7C,cAAc,oBAAoB,IAAI,IAAI;AAAA,EAC5C,CAAC;AACH;AAEA,eAAe,8BACb,WACA,OAUe;AACf,aAAW,YAAY,WAAW;AAChC,QAAI,CAAC,SAAS,MAAM,aAAc;AAClC,UAAM,SAAS,MAAM,aAAa;AAAA,MAChC,GAAG;AAAA,MACH,UAAU,SAAS,YAAY;AAAA,IACjC,CAAC;AAAA,EACH;AACF;AAEA,eAAe,sBAAsB,KAAuC;AAC1E,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAEhD,MAAI,CAAC,QAAQ,CAAC,KAAK,UAAU;AAC3B,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,uCAAuC,cAAc,EAAE,CAAC;AAAA,EAC1G;AAEA,QAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACxF,QAAM,iBAAiB,OAAO,cAAc,KAAK,SAAS;AAC1D,MAAI,CAAC,gBAAgB;AACnB,UAAM,IAAI,cAAc,KAAK;AAAA,MAC3B,OAAO,UAAU,gDAAgD,kCAAkC;AAAA,IACrG,CAAC;AAAA,EACH;AAEA,QAAM,MAA6B;AAAA,IACjC;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,IACnB,wBAAwB;AAAA,IACxB,iBAAiB,OAAO,cAAc,KAAK,QAAQ,CAAC,KAAK,KAAK,IAAI;AAAA,IAClE,SAAS;AAAA,EACX;AAEA,SAAO,EAAE,IAAI;AACf;AAEA,SAAS,kBAAkB,OAAkC;AAC3D,QAAM,WAAW,MAAM,oBAAoB,OAAO,MAAM,qBAAqB,WAAY,MAAM,mBAA+C;AAC9I,QAAMA,YAAW,MAAM,YAAY,OAAO,MAAM,aAAa,WAAY,MAAM,WAAuC;AACtH,QAAM,UAAU,UAAU;AAC1B,QAAM,WAAW,UAAU;AAC3B,QAAM,YACH,OAAO,SAAS,UAAU,YAAY,QAAQ,MAAM,KAAK,KACzD,OAAO,UAAU,iBAAiB,YAAY,SAAS,aAAa,KAAK,KACzE,OAAOA,WAAU,kBAAkB,YAAYA,UAAS,cAAc,KAAK,KAC5E;AACF,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,SAAS;AACrD,SAAO,OAAO,UAAU,OAAO,OAAO;AACxC;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,EAAE,IAAI,IAAI,MAAM,sBAAsB,GAAG;AAC/C,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,UAAU,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACjD,UAAM,SAAS,kBAAkB,WAAW,CAAC,GAAG,KAAK,SAAS;AAC9D,UAAM,QAAQ,gBAAgB,MAAM,MAAM;AAC1C,UAAM,cAAc,MAAM,UAAU,KAAK;AAAA,MACvC,UAAU,IAAI,MAAM,YAAY;AAAA,MAChC,gBAAgB,IAAI,0BAA0B,IAAI,MAAM,SAAS;AAAA,MACjE,QAAQ,IAAI,MAAM,OAAO;AAAA,MACzB,cAAc;AAAA,MACd,YAAY,MAAM;AAAA,MAClB,WAAW;AAAA,MACX,eAAe,IAAI;AAAA,MACnB,gBAAgB,IAAI;AAAA,IACtB,CAAC;AACD,QAAI,CAAC,YAAY,IAAI;AACnB,aAAO,aAAa,KAAK,YAAY,aAAa,EAAE,OAAO,6BAA6B,GAAG,EAAE,QAAQ,YAAY,eAAe,IAAI,CAAC;AAAA,IACvI;AAEA,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,QAAQ,MAAM,GAAG,QAAQ,YAAY,EAAE,IAAI,MAAM,SAAS,WAAW,KAAK,CAAC;AACjF,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,gCAAgC,qCAAqC,EAAE,CAAC;AAAA,IAC1H;AACA,QAAI,MAAM,aAAa,IAAI,MAAM,YAAY,MAAM,mBAAmB,IAAI,wBAAwB;AAChG,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,oCAAoC,WAAW,EAAE,CAAC;AAAA,IACpG;AAEA,SAAK,MAAM,UAAU,UAAU,YAAY;AACzC,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,8BAA8B,iCAAiC,EAAE,CAAC;AAAA,IACpH;AAEA,UAAM,QAAQ,kBAAkB,KAAK;AACrC,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,kCAAkC,6CAA6C,EAAE,CAAC;AAAA,IACpI;AAEA,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,aAAa,IAAI,KAAK,GAAG;AAC/B,eAAW,WAAW,WAAW,WAAW,IAAI,MAAM,YAAY;AAElE,UAAM,aAAa;AACnB,UAAM,kBAAkB,OAAO,WAAW;AAC1C,UAAM,SAAS;AACf,UAAM,SAAS;AACf,UAAM,gBAAgB,MAAM,4BAA4B,IAAI;AAAA,MAC1D,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB,OAAO;AAAA,IACT,CAAC;AACD,UAAM,YAAY;AAClB,OAAG,QAAQ,KAAK;AAChB,UAAM,GAAG,MAAM;AAEf,UAAM,SAAS,QAAQ,IAAI,WAAW;AACtC,UAAM,MAAM,SAAS,GAAG,OAAO,QAAQ,OAAO,EAAE,CAAC,UAAU,MAAM,eAAe,KAAK,UAAU,MAAM,eAAe;AAEpH,UAAM,SAAS,MAAM,aAAa;AAClC,UAAM,sBAAsB,WAAW,mBAAmB,QAAQ;AAAA,MAChE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,KAAK;AAAA,IACP,CAAC;AAED,UAAM,OAAO;AAAA,MACX,SAAS,UAAU,8BAA8B,2CAA2C,EAAE,aAAa,MAAM,YAAY,CAAC;AAAA,MAC9H,SAAS,UAAU,8BAA8B,uBAAuB,EAAE,aAAa,MAAM,YAAY,CAAC;AAAA,MAC1G,OAAO,UAAU,4BAA4B,8BAA8B;AAAA,QACzE,QAAQ,MAAM,yBAAyB,MAAM,uBAAuB;AAAA,QACpE,UAAU,MAAM;AAAA,MAClB,CAAC;AAAA,MACD,YAAY,UAAU,iCAAiC,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAAA,MAC3G,KAAK,UAAU,0BAA0B,YAAY;AAAA,MACrD,QAAQ,UAAU,6BAA6B,cAAc;AAAA,IAC/D;AAEA,UAAM,UAAU;AAAA,MACd,IAAI;AAAA,MACJ,SAAS,UAAU,8BAA8B,uBAAuB,EAAE,aAAa,MAAM,YAAY,CAAC;AAAA,MAC1G,OAAO,eAAe,EAAE,KAAK,KAAK,CAAC;AAAA,IACrC,CAAC;AAED,QAAI,YAAY,sBAAsB,QAAQ;AAC5C,YAAM,8BAA8B,YAAY,uBAAuB;AAAA,QACrE,UAAU,IAAI,MAAM,YAAY;AAAA,QAChC,gBAAgB,IAAI,0BAA0B,IAAI,MAAM,SAAS;AAAA,QACjE,QAAQ,IAAI,MAAM,OAAO;AAAA,QACzB,cAAc;AAAA,QACd,YAAY,MAAM;AAAA,QAClB,WAAW;AAAA,QACX,eAAe,IAAI;AAAA,QACnB,gBAAgB,IAAI;AAAA,MACtB,CAAC;AAAA,IACH;AAEA,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC,SAAS,KAAK;AACZ,QAAI,eAAe,eAAe;AAChC,aAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IAC3D;AACA,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,YAAQ,MAAM,4BAA4B,GAAG;AAC7C,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,UAAU,4BAA4B,uBAAuB,EAAE;AAAA,MACxE,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC,EAAE;AAAA,QACtF,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,QACvF,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,QACpF,EAAE,QAAQ,KAAK,aAAa,aAAa,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,QACjF,EAAE,QAAQ,KAAK,aAAa,aAAa,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,QACjF,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE;AAAA,QACtH,EAAE,QAAQ,KAAK,aAAa,iBAAiB,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE;AAAA,MACpH;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": ["metadata"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.4.6-develop-bbfd75b1c8",
3
+ "version": "0.4.6-develop-2ba4e02ffb",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -207,7 +207,7 @@
207
207
  }
208
208
  },
209
209
  "dependencies": {
210
- "@open-mercato/shared": "0.4.6-develop-bbfd75b1c8",
210
+ "@open-mercato/shared": "0.4.6-develop-2ba4e02ffb",
211
211
  "@types/html-to-text": "^9.0.4",
212
212
  "@types/semver": "^7.5.8",
213
213
  "@xyflow/react": "^12.6.0",
@@ -4,7 +4,7 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
4
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
5
  import type { EntityManager } from '@mikro-orm/postgresql'
6
6
  import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
7
- import { SyncMapping } from '../../../data/entities'
7
+ import { SyncMapping } from '@open-mercato/core/modules/data_sync/data/entities'
8
8
 
9
9
  const idParamsSchema = z.object({ id: z.string().uuid() })
10
10
 
@@ -13,7 +13,7 @@ const updateMappingSchema = z.object({
13
13
  })
14
14
 
15
15
  export const metadata = {
16
- GET: { requireAuth: true, requireFeatures: ['data_sync.view'] },
16
+ GET: { requireAuth: true, requireFeatures: ['data_sync.configure'] },
17
17
  PUT: { requireAuth: true, requireFeatures: ['data_sync.configure'] },
18
18
  DELETE: { requireAuth: true, requireFeatures: ['data_sync.configure'] },
19
19
  }
@@ -4,7 +4,7 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
4
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
5
  import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
6
6
  import { findAndCountWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
7
- import { SyncMapping } from '../../data/entities'
7
+ import { SyncMapping } from '@open-mercato/core/modules/data_sync/data/entities'
8
8
 
9
9
  const listMappingsQuerySchema = z.object({
10
10
  integrationId: z.string().min(1).optional(),
@@ -20,7 +20,7 @@ const createMappingSchema = z.object({
20
20
  })
21
21
 
22
22
  export const metadata = {
23
- GET: { requireAuth: true, requireFeatures: ['data_sync.view'] },
23
+ GET: { requireAuth: true, requireFeatures: ['data_sync.configure'] },
24
24
  POST: { requireAuth: true, requireFeatures: ['data_sync.configure'] },
25
25
  }
26
26
 
@@ -52,9 +52,17 @@ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }>
52
52
  ? await Promise.all(
53
53
  getBundleIntegrations(integration.bundleId).map(async (item) => {
54
54
  const itemState = await stateService.get(item.id, scope)
55
+ const resolvedState = {
56
+ isEnabled: itemState?.isEnabled ?? true,
57
+ apiVersion: itemState?.apiVersion ?? null,
58
+ reauthRequired: itemState?.reauthRequired ?? false,
59
+ lastHealthStatus: itemState?.lastHealthStatus ?? null,
60
+ lastHealthCheckedAt: itemState?.lastHealthCheckedAt?.toISOString() ?? null,
61
+ }
55
62
  return {
56
63
  ...item,
57
- isEnabled: itemState?.isEnabled ?? true,
64
+ isEnabled: resolvedState.isEnabled,
65
+ state: resolvedState,
58
66
  }
59
67
  }),
60
68
  )
@@ -1,11 +1,11 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
3
3
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
- import { listIntegrationLogsQuerySchema } from '../../data/validators'
5
- import type { IntegrationLogService } from '../../lib/log-service'
4
+ import { listIntegrationLogsQuerySchema } from '@open-mercato/core/modules/integrations/data/validators'
5
+ import type { IntegrationLogService } from '@open-mercato/core/modules/integrations/lib/log-service'
6
6
 
7
7
  export const metadata = {
8
- GET: { requireAuth: true, requireFeatures: ['integrations.view'] },
8
+ GET: { requireAuth: true, requireFeatures: ['integrations.manage'] },
9
9
  }
10
10
 
11
11
  export const openApi = {
@@ -13,12 +13,18 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@open-mercato/ui/primi
13
13
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
14
14
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
15
15
  import { useT } from '@open-mercato/shared/lib/i18n/context'
16
- import type { IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'
16
+ import type { CredentialFieldType, IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'
17
17
  import { LoadingMessage } from '@open-mercato/ui/backend/detail'
18
18
  import { ErrorMessage } from '@open-mercato/ui/backend/detail'
19
19
 
20
20
  type CredentialField = IntegrationCredentialField
21
21
 
22
+ const UNSUPPORTED_CREDENTIAL_FIELD_TYPES = new Set<CredentialFieldType>(['oauth', 'ssh_keypair'])
23
+
24
+ function isEditableCredentialField(field: CredentialField): boolean {
25
+ return !UNSUPPORTED_CREDENTIAL_FIELD_TYPES.has(field.type)
26
+ }
27
+
22
28
  type ApiVersion = {
23
29
  id: string
24
30
  label?: string
@@ -261,7 +267,7 @@ export default function IntegrationDetailPage() {
261
267
  ) : (
262
268
  <Card>
263
269
  <CardContent className="pt-6 space-y-4">
264
- {credFields.filter((f) => f.type !== 'oauth' && f.type !== 'ssh_keypair').map((field) => (
270
+ {credFields.filter(isEditableCredentialField).map((field) => (
265
271
  <div key={field.key} className="space-y-1.5">
266
272
  <label className="text-sm font-medium">
267
273
  {field.label}{field.required && <span className="text-red-500 ml-0.5">*</span>}