@open-mercato/core 0.6.2-develop.3330.1.8ad2b3b84c → 0.6.2-develop.3366.1.4692954e44

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 (102) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/sync_excel_upload/index.js +37 -0
  3. package/dist/generated/entities/sync_excel_upload/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +4 -0
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +19 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/customers/search.js +3 -2
  9. package/dist/modules/customers/search.js.map +2 -2
  10. package/dist/modules/data_sync/api/runs/[id]/cancel.js +16 -0
  11. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  12. package/dist/modules/data_sync/di.js +2 -1
  13. package/dist/modules/data_sync/di.js.map +2 -2
  14. package/dist/modules/data_sync/lib/sync-engine.js +178 -54
  15. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  16. package/dist/modules/data_sync/lib/sync-run-service.js +23 -0
  17. package/dist/modules/data_sync/lib/sync-run-service.js.map +3 -3
  18. package/dist/modules/integrations/backend/integrations/[id]/page.js +241 -55
  19. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +3 -3
  20. package/dist/modules/sync_excel/acl.js +10 -0
  21. package/dist/modules/sync_excel/acl.js.map +7 -0
  22. package/dist/modules/sync_excel/api/import/route.js +164 -0
  23. package/dist/modules/sync_excel/api/import/route.js.map +7 -0
  24. package/dist/modules/sync_excel/api/preview/route.js +84 -0
  25. package/dist/modules/sync_excel/api/preview/route.js.map +7 -0
  26. package/dist/modules/sync_excel/api/upload/route.js +131 -0
  27. package/dist/modules/sync_excel/api/upload/route.js.map +7 -0
  28. package/dist/modules/sync_excel/data/entities.js +79 -0
  29. package/dist/modules/sync_excel/data/entities.js.map +7 -0
  30. package/dist/modules/sync_excel/data/validators.js +58 -0
  31. package/dist/modules/sync_excel/data/validators.js.map +7 -0
  32. package/dist/modules/sync_excel/di.js +11 -0
  33. package/dist/modules/sync_excel/di.js.map +7 -0
  34. package/dist/modules/sync_excel/index.js +11 -0
  35. package/dist/modules/sync_excel/index.js.map +7 -0
  36. package/dist/modules/sync_excel/integration.js +36 -0
  37. package/dist/modules/sync_excel/integration.js.map +7 -0
  38. package/dist/modules/sync_excel/lib/adapters/customers.js +852 -0
  39. package/dist/modules/sync_excel/lib/adapters/customers.js.map +7 -0
  40. package/dist/modules/sync_excel/lib/column-detector.js +77 -0
  41. package/dist/modules/sync_excel/lib/column-detector.js.map +7 -0
  42. package/dist/modules/sync_excel/lib/mapping.js +16 -0
  43. package/dist/modules/sync_excel/lib/mapping.js.map +7 -0
  44. package/dist/modules/sync_excel/lib/parser.js +125 -0
  45. package/dist/modules/sync_excel/lib/parser.js.map +7 -0
  46. package/dist/modules/sync_excel/lib/scope.js +44 -0
  47. package/dist/modules/sync_excel/lib/scope.js.map +7 -0
  48. package/dist/modules/sync_excel/lib/upload-storage.js +63 -0
  49. package/dist/modules/sync_excel/lib/upload-storage.js.map +7 -0
  50. package/dist/modules/sync_excel/migrations/Migration20260329181120_sync_excel.js +11 -0
  51. package/dist/modules/sync_excel/migrations/Migration20260329181120_sync_excel.js.map +7 -0
  52. package/dist/modules/sync_excel/setup.js +19 -0
  53. package/dist/modules/sync_excel/setup.js.map +7 -0
  54. package/dist/modules/sync_excel/widgets/injection/upload-config/target-options.js +293 -0
  55. package/dist/modules/sync_excel/widgets/injection/upload-config/target-options.js.map +7 -0
  56. package/dist/modules/sync_excel/widgets/injection/upload-config/widget.client.js +934 -0
  57. package/dist/modules/sync_excel/widgets/injection/upload-config/widget.client.js.map +7 -0
  58. package/dist/modules/sync_excel/widgets/injection/upload-config/widget.js +15 -0
  59. package/dist/modules/sync_excel/widgets/injection/upload-config/widget.js.map +7 -0
  60. package/dist/modules/sync_excel/widgets/injection-table.js +17 -0
  61. package/dist/modules/sync_excel/widgets/injection-table.js.map +7 -0
  62. package/generated/entities/sync_excel_upload/index.ts +17 -0
  63. package/generated/entities.ids.generated.ts +4 -0
  64. package/generated/entity-fields-registry.ts +19 -0
  65. package/package.json +7 -7
  66. package/src/modules/customers/search.ts +3 -2
  67. package/src/modules/data_sync/api/runs/[id]/cancel.ts +18 -0
  68. package/src/modules/data_sync/di.ts +4 -1
  69. package/src/modules/data_sync/lib/adapter.ts +15 -0
  70. package/src/modules/data_sync/lib/sync-engine.ts +205 -52
  71. package/src/modules/data_sync/lib/sync-run-service.ts +24 -0
  72. package/src/modules/integrations/backend/integrations/[id]/page.tsx +220 -15
  73. package/src/modules/integrations/i18n/de.json +9 -0
  74. package/src/modules/integrations/i18n/en.json +9 -0
  75. package/src/modules/integrations/i18n/es.json +9 -0
  76. package/src/modules/integrations/i18n/pl.json +9 -0
  77. package/src/modules/sync_excel/acl.ts +6 -0
  78. package/src/modules/sync_excel/api/import/route.ts +182 -0
  79. package/src/modules/sync_excel/api/preview/route.ts +89 -0
  80. package/src/modules/sync_excel/api/upload/route.ts +144 -0
  81. package/src/modules/sync_excel/data/entities.ts +61 -0
  82. package/src/modules/sync_excel/data/validators.ts +58 -0
  83. package/src/modules/sync_excel/di.ts +9 -0
  84. package/src/modules/sync_excel/i18n/de.json +107 -0
  85. package/src/modules/sync_excel/i18n/en.json +107 -0
  86. package/src/modules/sync_excel/i18n/es.json +107 -0
  87. package/src/modules/sync_excel/i18n/pl.json +107 -0
  88. package/src/modules/sync_excel/index.ts +7 -0
  89. package/src/modules/sync_excel/integration.ts +31 -0
  90. package/src/modules/sync_excel/lib/adapters/customers.ts +1097 -0
  91. package/src/modules/sync_excel/lib/column-detector.ts +91 -0
  92. package/src/modules/sync_excel/lib/mapping.ts +19 -0
  93. package/src/modules/sync_excel/lib/parser.ts +165 -0
  94. package/src/modules/sync_excel/lib/scope.ts +63 -0
  95. package/src/modules/sync_excel/lib/upload-storage.ts +79 -0
  96. package/src/modules/sync_excel/migrations/.snapshot-open-mercato.json +199 -0
  97. package/src/modules/sync_excel/migrations/Migration20260329181120_sync_excel.ts +10 -0
  98. package/src/modules/sync_excel/setup.ts +20 -0
  99. package/src/modules/sync_excel/widgets/injection/upload-config/target-options.ts +351 -0
  100. package/src/modules/sync_excel/widgets/injection/upload-config/widget.client.tsx +1224 -0
  101. package/src/modules/sync_excel/widgets/injection/upload-config/widget.ts +30 -0
  102. package/src/modules/sync_excel/widgets/injection-table.ts +15 -0
@@ -0,0 +1,91 @@
1
+ import type { DataMapping, FieldMapping } from '../../data_sync/lib/adapter'
2
+
3
+ const PERSON_FIELD_ALIASES: Record<string, string[]> = {
4
+ 'person.externalId': ['record id', 'external id', 'lead id'],
5
+ 'person.firstName': ['first name', 'firstname', 'given name'],
6
+ 'person.lastName': ['last name', 'lastname', 'surname', 'family name'],
7
+ 'person.displayName': ['lead name', 'display name', 'full name', 'name'],
8
+ 'person.primaryEmail': ['email', 'primary email', 'email address'],
9
+ 'person.primaryPhone': ['mobile', 'phone', 'mobile phone', 'telephone'],
10
+ 'person.jobTitle': ['title', 'job title', 'position'],
11
+ 'person.status': ['lead status', 'status'],
12
+ 'person.source': ['lead source', 'source'],
13
+ 'person.description': ['description', 'notes', 'comment'],
14
+ 'address.name': ['address label', 'address name', 'label'],
15
+ 'address.purpose': ['address purpose', 'address type'],
16
+ 'address.companyName': ['address company', 'address company name'],
17
+ 'address.addressLine1': ['address line 1', 'street address', 'street', 'street 1'],
18
+ 'address.addressLine2': ['address line 2', 'street 2'],
19
+ 'address.buildingNumber': ['building number', 'building no', 'house number'],
20
+ 'address.flatNumber': ['flat number', 'apartment number', 'unit number'],
21
+ 'address.city': ['city', 'town'],
22
+ 'address.region': ['region', 'state', 'province'],
23
+ 'address.postalCode': ['postal code', 'zip code', 'postcode'],
24
+ 'address.country': ['country'],
25
+ 'address.latitude': ['latitude', 'lat'],
26
+ 'address.longitude': ['longitude', 'lng', 'lon'],
27
+ }
28
+
29
+ function normalizeLabel(value: string): string {
30
+ return value
31
+ .trim()
32
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
33
+ .toLowerCase()
34
+ .replace(/[_-]+/g, ' ')
35
+ .replace(/\s+/g, ' ')
36
+ .trim()
37
+ }
38
+
39
+ function buildFieldMapping(externalField: string, localField: string): FieldMapping {
40
+ if (localField === 'person.externalId') {
41
+ return {
42
+ externalField,
43
+ localField,
44
+ mappingKind: 'external_id',
45
+ dedupeRole: 'primary',
46
+ }
47
+ }
48
+
49
+ if (localField === 'person.primaryEmail') {
50
+ return {
51
+ externalField,
52
+ localField,
53
+ mappingKind: 'core',
54
+ dedupeRole: 'secondary',
55
+ }
56
+ }
57
+
58
+ return {
59
+ externalField,
60
+ localField,
61
+ mappingKind: 'core',
62
+ }
63
+ }
64
+
65
+ export function detectCustomersPersonMapping(columns: string[]): DataMapping & { unmappedColumns: string[] } {
66
+ const fields: FieldMapping[] = []
67
+ const unmappedColumns: string[] = []
68
+
69
+ for (const column of columns) {
70
+ const normalizedColumn = normalizeLabel(column)
71
+ const matchingField = Object.entries(PERSON_FIELD_ALIASES).find(([, aliases]) => aliases.includes(normalizedColumn))
72
+
73
+ if (!matchingField) {
74
+ unmappedColumns.push(column)
75
+ continue
76
+ }
77
+
78
+ fields.push(buildFieldMapping(column, matchingField[0]))
79
+ }
80
+
81
+ const hasExternalId = fields.some((field) => field.localField === 'person.externalId')
82
+ const hasEmail = fields.some((field) => field.localField === 'person.primaryEmail')
83
+
84
+ return {
85
+ entityType: 'customers.person',
86
+ matchStrategy: hasExternalId ? 'externalId' : hasEmail ? 'email' : 'custom',
87
+ matchField: hasExternalId ? 'person.externalId' : hasEmail ? 'person.primaryEmail' : undefined,
88
+ fields,
89
+ unmappedColumns,
90
+ }
91
+ }
@@ -0,0 +1,19 @@
1
+ import type { DataMapping, FieldMapping } from '../../data_sync/lib/adapter'
2
+ import { detectCustomersPersonMapping } from './column-detector'
3
+
4
+ export type SuggestedMapping = DataMapping & {
5
+ unmappedColumns: string[]
6
+ }
7
+
8
+ export function buildSuggestedMapping(entityType: string, headers: string[]): SuggestedMapping {
9
+ if (entityType === 'customers.person') {
10
+ return detectCustomersPersonMapping(headers)
11
+ }
12
+
13
+ return {
14
+ entityType,
15
+ matchStrategy: 'custom',
16
+ fields: [] as FieldMapping[],
17
+ unmappedColumns: headers.slice(),
18
+ }
19
+ }
@@ -0,0 +1,165 @@
1
+ export type CsvPreviewRow = Record<string, string | null>
2
+
3
+ export type CsvPreview = {
4
+ headers: string[]
5
+ sampleRows: CsvPreviewRow[]
6
+ totalRows: number
7
+ delimiter: ',' | ';'
8
+ encoding: 'utf-8'
9
+ }
10
+
11
+ export type CsvDocument = {
12
+ headers: string[]
13
+ rows: CsvPreviewRow[]
14
+ totalRows: number
15
+ delimiter: ',' | ';'
16
+ encoding: 'utf-8'
17
+ }
18
+
19
+ type ParseCsvPreviewOptions = {
20
+ maxRows?: number
21
+ }
22
+
23
+ function stripUtf8Bom(value: string): string {
24
+ return value.charCodeAt(0) === 0xfeff ? value.slice(1) : value
25
+ }
26
+
27
+ function countFields(line: string, delimiter: ',' | ';'): number {
28
+ let count = 1
29
+ let inQuotes = false
30
+
31
+ for (let index = 0; index < line.length; index += 1) {
32
+ const character = line[index]
33
+ if (character === '"') {
34
+ if (inQuotes && line[index + 1] === '"') {
35
+ index += 1
36
+ continue
37
+ }
38
+ inQuotes = !inQuotes
39
+ continue
40
+ }
41
+ if (!inQuotes && character === delimiter) {
42
+ count += 1
43
+ }
44
+ }
45
+
46
+ return count
47
+ }
48
+
49
+ export function detectCsvDelimiter(text: string): ',' | ';' {
50
+ const normalized = stripUtf8Bom(text).replace(/\r\n/g, '\n').replace(/\r/g, '\n')
51
+ const lines = normalized
52
+ .split('\n')
53
+ .map((line) => line.trim())
54
+ .filter((line) => line.length > 0)
55
+ .slice(0, 5)
56
+
57
+ if (lines.length === 0) return ','
58
+
59
+ const commaScore = lines.reduce((accumulator, line) => accumulator + countFields(line, ','), 0)
60
+ const semicolonScore = lines.reduce((accumulator, line) => accumulator + countFields(line, ';'), 0)
61
+
62
+ return semicolonScore > commaScore ? ';' : ','
63
+ }
64
+
65
+ export function parseCsvText(text: string, delimiter: ',' | ';'): string[][] {
66
+ const normalized = stripUtf8Bom(text).replace(/\r\n/g, '\n').replace(/\r/g, '\n')
67
+ const rows: string[][] = []
68
+ let currentRow: string[] = []
69
+ let currentValue = ''
70
+ let inQuotes = false
71
+
72
+ for (let index = 0; index < normalized.length; index += 1) {
73
+ const character = normalized[index]
74
+
75
+ if (character === '"') {
76
+ if (inQuotes && normalized[index + 1] === '"') {
77
+ currentValue += '"'
78
+ index += 1
79
+ continue
80
+ }
81
+ inQuotes = !inQuotes
82
+ continue
83
+ }
84
+
85
+ if (!inQuotes && character === delimiter) {
86
+ currentRow.push(currentValue)
87
+ currentValue = ''
88
+ continue
89
+ }
90
+
91
+ if (!inQuotes && character === '\n') {
92
+ currentRow.push(currentValue)
93
+ const isMeaningfulRow = currentRow.some((value) => value.trim().length > 0)
94
+ if (isMeaningfulRow) {
95
+ rows.push(currentRow)
96
+ }
97
+ currentRow = []
98
+ currentValue = ''
99
+ continue
100
+ }
101
+
102
+ currentValue += character
103
+ }
104
+
105
+ if (currentValue.length > 0 || currentRow.length > 0) {
106
+ currentRow.push(currentValue)
107
+ const isMeaningfulRow = currentRow.some((value) => value.trim().length > 0)
108
+ if (isMeaningfulRow) {
109
+ rows.push(currentRow)
110
+ }
111
+ }
112
+
113
+ return rows
114
+ }
115
+
116
+ function normalizeHeaders(headers: string[]): string[] {
117
+ return headers.map((header, index) => {
118
+ const trimmed = header.trim()
119
+ return trimmed.length > 0 ? trimmed : `Column ${index + 1}`
120
+ })
121
+ }
122
+
123
+ function rowsToObjects(headers: string[], rows: string[][], maxRows: number): CsvPreviewRow[] {
124
+ return rows.slice(0, maxRows).map((row) => {
125
+ const record: CsvPreviewRow = {}
126
+ headers.forEach((header, index) => {
127
+ const value = row[index]
128
+ record[header] = typeof value === 'string' && value.length > 0 ? value : null
129
+ })
130
+ return record
131
+ })
132
+ }
133
+
134
+ export function parseCsvPreview(buffer: Buffer, options: ParseCsvPreviewOptions = {}): CsvPreview {
135
+ const text = buffer.toString('utf-8')
136
+ const delimiter = detectCsvDelimiter(text)
137
+ const rows = parseCsvText(text, delimiter)
138
+ const [rawHeaders = [], ...dataRows] = rows
139
+ const headers = normalizeHeaders(rawHeaders)
140
+ const maxRows = options.maxRows ?? 5
141
+
142
+ return {
143
+ headers,
144
+ sampleRows: rowsToObjects(headers, dataRows, maxRows),
145
+ totalRows: dataRows.length,
146
+ delimiter,
147
+ encoding: 'utf-8',
148
+ }
149
+ }
150
+
151
+ export function parseCsvDocument(buffer: Buffer): CsvDocument {
152
+ const text = buffer.toString('utf-8')
153
+ const delimiter = detectCsvDelimiter(text)
154
+ const rows = parseCsvText(text, delimiter)
155
+ const [rawHeaders = [], ...dataRows] = rows
156
+ const headers = normalizeHeaders(rawHeaders)
157
+
158
+ return {
159
+ headers,
160
+ rows: rowsToObjects(headers, dataRows, dataRows.length),
161
+ totalRows: dataRows.length,
162
+ delimiter,
163
+ encoding: 'utf-8',
164
+ }
165
+ }
@@ -0,0 +1,63 @@
1
+ import type { AwilixContainer } from 'awilix'
2
+ import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
3
+ import { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'
4
+ import { getSelectedOrganizationFromRequest, resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
5
+ import type { TenantScope } from '../../data_sync/lib/adapter'
6
+
7
+ export type SyncExcelConcreteScopeResult =
8
+ | { ok: true; scope: TenantScope }
9
+ | { ok: false; status: 401 | 422; error: string }
10
+
11
+ const CONCRETE_ORGANIZATION_REQUIRED_ERROR = 'Select a concrete organization before importing CSV.'
12
+
13
+ function normalizeSelectedOrganizationId(value: string | null | undefined): string | null {
14
+ if (typeof value !== 'string') return null
15
+ const trimmed = value.trim()
16
+ return trimmed.length > 0 ? trimmed : null
17
+ }
18
+
19
+ export async function resolveSyncExcelConcreteScope(params: {
20
+ auth: AuthContext
21
+ container: AwilixContainer
22
+ request: Request
23
+ }): Promise<SyncExcelConcreteScopeResult> {
24
+ const { auth, container, request } = params
25
+ if (!auth?.tenantId) {
26
+ return { ok: false, status: 401, error: 'Unauthorized' }
27
+ }
28
+
29
+ const requestedSelectedId = normalizeSelectedOrganizationId(getSelectedOrganizationFromRequest(request))
30
+ if (!requestedSelectedId || isAllOrganizationsSelection(requestedSelectedId)) {
31
+ return { ok: false, status: 422, error: CONCRETE_ORGANIZATION_REQUIRED_ERROR }
32
+ }
33
+
34
+ const organizationScope = await resolveOrganizationScopeForRequest({
35
+ container,
36
+ auth,
37
+ request,
38
+ selectedId: requestedSelectedId,
39
+ })
40
+
41
+ const selectedId = organizationScope.selectedId
42
+ const tenantId = organizationScope.tenantId ?? auth.tenantId
43
+ const filterIds = organizationScope.filterIds
44
+ const selectedIsResolvable = Boolean(
45
+ selectedId
46
+ && tenantId
47
+ && Array.isArray(filterIds)
48
+ && selectedId === requestedSelectedId
49
+ && filterIds.includes(selectedId),
50
+ )
51
+
52
+ if (!selectedIsResolvable || !selectedId || !tenantId) {
53
+ return { ok: false, status: 422, error: CONCRETE_ORGANIZATION_REQUIRED_ERROR }
54
+ }
55
+
56
+ return {
57
+ ok: true,
58
+ scope: {
59
+ organizationId: selectedId,
60
+ tenantId,
61
+ },
62
+ }
63
+ }
@@ -0,0 +1,79 @@
1
+ import { promises as fs } from 'fs'
2
+ import { randomUUID } from 'crypto'
3
+ import type { EntityManager } from '@mikro-orm/postgresql'
4
+ import { Attachment, AttachmentPartition } from '../../attachments/data/entities'
5
+ import { buildAttachmentFileUrl } from '../../attachments/lib/imageUrls'
6
+ import { ensureDefaultPartitions } from '../../attachments/lib/partitions'
7
+ import { resolveAttachmentAbsolutePath, storePartitionFile } from '../../attachments/lib/storage'
8
+
9
+ const SYNC_EXCEL_ATTACHMENT_ENTITY_ID = 'sync_excel:upload'
10
+ const SYNC_EXCEL_PARTITION_CODE = 'privateAttachments'
11
+
12
+ export async function createSyncExcelUploadAttachment(input: {
13
+ em: EntityManager
14
+ uploadId: string
15
+ organizationId: string
16
+ tenantId: string
17
+ fileName: string
18
+ mimeType: string
19
+ buffer: Buffer
20
+ }): Promise<Attachment> {
21
+ await ensureDefaultPartitions(input.em)
22
+
23
+ // Attachment partitions are global storage configuration, not tenant-scoped payload data.
24
+ const partition = await input.em.findOne(AttachmentPartition, { code: SYNC_EXCEL_PARTITION_CODE })
25
+ if (!partition) {
26
+ throw new Error('Storage partition is not configured.')
27
+ }
28
+
29
+ const stored = await storePartitionFile({
30
+ partitionCode: partition.code,
31
+ orgId: input.organizationId,
32
+ tenantId: input.tenantId,
33
+ fileName: input.fileName,
34
+ buffer: input.buffer,
35
+ })
36
+
37
+ const attachmentId = randomUUID()
38
+ const attachment = input.em.create(Attachment, {
39
+ id: attachmentId,
40
+ entityId: SYNC_EXCEL_ATTACHMENT_ENTITY_ID,
41
+ recordId: input.uploadId,
42
+ organizationId: input.organizationId,
43
+ tenantId: input.tenantId,
44
+ partitionCode: partition.code,
45
+ fileName: input.fileName,
46
+ mimeType: input.mimeType,
47
+ fileSize: input.buffer.length,
48
+ storageDriver: partition.storageDriver || 'local',
49
+ storagePath: stored.storagePath,
50
+ url: buildAttachmentFileUrl(attachmentId),
51
+ storageMetadata: {
52
+ module: 'sync_excel',
53
+ temporary: true,
54
+ uploadId: input.uploadId,
55
+ inlineCsvBase64: input.buffer.toString('base64'),
56
+ },
57
+ })
58
+
59
+ input.em.persist(attachment)
60
+ await input.em.flush()
61
+
62
+ return attachment
63
+ }
64
+
65
+ export async function readSyncExcelUploadBuffer(
66
+ attachment: Pick<Attachment, 'partitionCode' | 'storagePath' | 'storageDriver' | 'storageMetadata'>,
67
+ ): Promise<Buffer> {
68
+ const inlineCsvBase64 = attachment.storageMetadata?.inlineCsvBase64
69
+ if (typeof inlineCsvBase64 === 'string' && inlineCsvBase64.length > 0) {
70
+ return Buffer.from(inlineCsvBase64, 'base64')
71
+ }
72
+
73
+ const absolutePath = resolveAttachmentAbsolutePath(
74
+ attachment.partitionCode,
75
+ attachment.storagePath,
76
+ attachment.storageDriver,
77
+ )
78
+ return fs.readFile(absolutePath)
79
+ }
@@ -0,0 +1,199 @@
1
+ {
2
+ "namespaces": [
3
+ "public"
4
+ ],
5
+ "name": "public",
6
+ "tables": [
7
+ {
8
+ "columns": {
9
+ "id": {
10
+ "name": "id",
11
+ "type": "uuid",
12
+ "unsigned": false,
13
+ "autoincrement": false,
14
+ "primary": false,
15
+ "nullable": false,
16
+ "default": "gen_random_uuid()",
17
+ "mappedType": "uuid"
18
+ },
19
+ "attachment_id": {
20
+ "name": "attachment_id",
21
+ "type": "uuid",
22
+ "unsigned": false,
23
+ "autoincrement": false,
24
+ "primary": false,
25
+ "nullable": false,
26
+ "mappedType": "uuid"
27
+ },
28
+ "filename": {
29
+ "name": "filename",
30
+ "type": "text",
31
+ "unsigned": false,
32
+ "autoincrement": false,
33
+ "primary": false,
34
+ "nullable": false,
35
+ "mappedType": "text"
36
+ },
37
+ "mime_type": {
38
+ "name": "mime_type",
39
+ "type": "text",
40
+ "unsigned": false,
41
+ "autoincrement": false,
42
+ "primary": false,
43
+ "nullable": false,
44
+ "mappedType": "text"
45
+ },
46
+ "file_size": {
47
+ "name": "file_size",
48
+ "type": "int",
49
+ "unsigned": false,
50
+ "autoincrement": false,
51
+ "primary": false,
52
+ "nullable": false,
53
+ "mappedType": "integer"
54
+ },
55
+ "entity_type": {
56
+ "name": "entity_type",
57
+ "type": "text",
58
+ "unsigned": false,
59
+ "autoincrement": false,
60
+ "primary": false,
61
+ "nullable": false,
62
+ "mappedType": "text"
63
+ },
64
+ "delimiter": {
65
+ "name": "delimiter",
66
+ "type": "text",
67
+ "unsigned": false,
68
+ "autoincrement": false,
69
+ "primary": false,
70
+ "nullable": true,
71
+ "mappedType": "text"
72
+ },
73
+ "encoding": {
74
+ "name": "encoding",
75
+ "type": "text",
76
+ "unsigned": false,
77
+ "autoincrement": false,
78
+ "primary": false,
79
+ "nullable": true,
80
+ "mappedType": "text"
81
+ },
82
+ "headers": {
83
+ "name": "headers",
84
+ "type": "jsonb",
85
+ "unsigned": false,
86
+ "autoincrement": false,
87
+ "primary": false,
88
+ "nullable": false,
89
+ "mappedType": "json"
90
+ },
91
+ "sample_rows": {
92
+ "name": "sample_rows",
93
+ "type": "jsonb",
94
+ "unsigned": false,
95
+ "autoincrement": false,
96
+ "primary": false,
97
+ "nullable": false,
98
+ "mappedType": "json"
99
+ },
100
+ "total_rows": {
101
+ "name": "total_rows",
102
+ "type": "int",
103
+ "unsigned": false,
104
+ "autoincrement": false,
105
+ "primary": false,
106
+ "nullable": false,
107
+ "mappedType": "integer"
108
+ },
109
+ "status": {
110
+ "name": "status",
111
+ "type": "text",
112
+ "unsigned": false,
113
+ "autoincrement": false,
114
+ "primary": false,
115
+ "nullable": false,
116
+ "default": "'uploaded'",
117
+ "mappedType": "text"
118
+ },
119
+ "sync_run_id": {
120
+ "name": "sync_run_id",
121
+ "type": "uuid",
122
+ "unsigned": false,
123
+ "autoincrement": false,
124
+ "primary": false,
125
+ "nullable": true,
126
+ "mappedType": "uuid"
127
+ },
128
+ "organization_id": {
129
+ "name": "organization_id",
130
+ "type": "uuid",
131
+ "unsigned": false,
132
+ "autoincrement": false,
133
+ "primary": false,
134
+ "nullable": false,
135
+ "mappedType": "uuid"
136
+ },
137
+ "tenant_id": {
138
+ "name": "tenant_id",
139
+ "type": "uuid",
140
+ "unsigned": false,
141
+ "autoincrement": false,
142
+ "primary": false,
143
+ "nullable": false,
144
+ "mappedType": "uuid"
145
+ },
146
+ "created_at": {
147
+ "name": "created_at",
148
+ "type": "timestamptz",
149
+ "unsigned": false,
150
+ "autoincrement": false,
151
+ "primary": false,
152
+ "nullable": false,
153
+ "length": 6,
154
+ "mappedType": "datetime"
155
+ },
156
+ "updated_at": {
157
+ "name": "updated_at",
158
+ "type": "timestamptz",
159
+ "unsigned": false,
160
+ "autoincrement": false,
161
+ "primary": false,
162
+ "nullable": false,
163
+ "length": 6,
164
+ "mappedType": "datetime"
165
+ }
166
+ },
167
+ "name": "sync_excel_uploads",
168
+ "schema": "public",
169
+ "indexes": [
170
+ {
171
+ "keyName": "sync_excel_uploads_organization_id_tenant_id_status_index",
172
+ "columnNames": [
173
+ "organization_id",
174
+ "tenant_id",
175
+ "status"
176
+ ],
177
+ "composite": true,
178
+ "constraint": false,
179
+ "primary": false,
180
+ "unique": false
181
+ },
182
+ {
183
+ "keyName": "sync_excel_uploads_pkey",
184
+ "columnNames": [
185
+ "id"
186
+ ],
187
+ "composite": false,
188
+ "constraint": true,
189
+ "primary": true,
190
+ "unique": true
191
+ }
192
+ ],
193
+ "checks": [],
194
+ "foreignKeys": {},
195
+ "nativeEnums": {}
196
+ }
197
+ ],
198
+ "nativeEnums": {}
199
+ }
@@ -0,0 +1,10 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260329181120_sync_excel extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`create table "sync_excel_uploads" ("id" uuid not null default gen_random_uuid(), "attachment_id" uuid not null, "filename" text not null, "mime_type" text not null, "file_size" int not null, "entity_type" text not null, "delimiter" text null, "encoding" text null, "headers" jsonb not null, "sample_rows" jsonb not null, "total_rows" int not null, "status" text not null default 'uploaded', "sync_run_id" uuid null, "organization_id" uuid not null, "tenant_id" uuid not null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "sync_excel_uploads_pkey" primary key ("id"));`);
7
+ this.addSql(`create index "sync_excel_uploads_organization_id_tenant_id_status_index" on "sync_excel_uploads" ("organization_id", "tenant_id", "status");`);
8
+ }
9
+
10
+ }
@@ -0,0 +1,20 @@
1
+ import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'
2
+ import type { CredentialsService } from '../integrations/lib/credentials-service'
3
+ import type { IntegrationStateService } from '../integrations/lib/state-service'
4
+
5
+ export const setup: ModuleSetupConfig = {
6
+ defaultRoleFeatures: {
7
+ superadmin: ['sync_excel.view', 'sync_excel.run'],
8
+ admin: ['sync_excel.view', 'sync_excel.run'],
9
+ },
10
+ async seedDefaults({ tenantId, organizationId, container }) {
11
+ const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService
12
+ const integrationStateService = container.resolve('integrationStateService') as IntegrationStateService
13
+ const scope = { tenantId, organizationId }
14
+
15
+ await credentialsService.save('sync_excel', {}, scope)
16
+ await integrationStateService.upsert('sync_excel', { isEnabled: true }, scope)
17
+ },
18
+ }
19
+
20
+ export default setup