@open-mercato/core 0.6.2-develop.3330.1.8ad2b3b84c → 0.6.2-develop.3363.1.e23f7084bd

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,852 @@
1
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
2
+ import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
+ import { extractPhoneDigits, validatePhoneNumber } from "@open-mercato/shared/lib/phone";
4
+ import { SyncMapping } from "../../../data_sync/data/entities.js";
5
+ import { CustomerAddress, CustomerEntity } from "../../../customers/data/entities.js";
6
+ import { Attachment } from "../../../attachments/data/entities.js";
7
+ import { CustomFieldDef } from "../../../entities/data/entities.js";
8
+ import { SyncExcelUpload } from "../../data/entities.js";
9
+ import { parseCsvDocument } from "../parser.js";
10
+ import { readSyncExcelUploadBuffer } from "../upload-storage.js";
11
+ import { E } from "../../../../generated/entities.ids.generated.js";
12
+ const CUSTOMER_IMPORT_COVERAGE_ENTITY_TYPES = [
13
+ E.customers.customer_entity,
14
+ E.customers.customer_person_profile,
15
+ E.customers.customer_address
16
+ ];
17
+ function normalizeOptionalString(value) {
18
+ if (typeof value !== "string") return null;
19
+ const trimmed = value.trim();
20
+ return trimmed.length > 0 ? trimmed : null;
21
+ }
22
+ function normalizeEmail(value) {
23
+ const normalized = normalizeOptionalString(value);
24
+ return normalized ? normalized.toLowerCase() : null;
25
+ }
26
+ const COUNTRY_DIAL_CODES = {
27
+ canada: "1",
28
+ england: "44",
29
+ greatbritain: "44",
30
+ poland: "48",
31
+ scotland: "44",
32
+ uk: "44",
33
+ unitedkingdom: "44",
34
+ unitedstates: "1",
35
+ unitedstatesofamerica: "1",
36
+ usa: "1",
37
+ wales: "44"
38
+ };
39
+ function normalizeCountryKey(value) {
40
+ const normalized = normalizeOptionalString(value);
41
+ if (!normalized) return null;
42
+ return normalized.toLowerCase().replace(/[^a-z]/g, "");
43
+ }
44
+ function resolveCountryDialCode(row) {
45
+ const countryCandidates = [
46
+ row.Country,
47
+ row.country,
48
+ row["Country/Region"],
49
+ row["country/region"]
50
+ ];
51
+ for (const candidate of countryCandidates) {
52
+ const countryKey = normalizeCountryKey(candidate);
53
+ if (!countryKey) continue;
54
+ const dialCode = COUNTRY_DIAL_CODES[countryKey];
55
+ if (dialCode) return dialCode;
56
+ }
57
+ return null;
58
+ }
59
+ function stripPhoneExtension(value) {
60
+ return value.replace(/\s*(ext\.?|extension|x)\s*\d+$/i, "").replace(/^\+\s+/, "+").replace(/\s+/g, " ").trim();
61
+ }
62
+ function normalizeImportedPhone(value, row) {
63
+ const normalized = normalizeOptionalString(value);
64
+ if (!normalized) return null;
65
+ const directValidation = validatePhoneNumber(normalized);
66
+ if (directValidation.valid) {
67
+ return directValidation.normalized;
68
+ }
69
+ const sanitized = stripPhoneExtension(normalized);
70
+ const sanitizedValidation = validatePhoneNumber(sanitized);
71
+ if (sanitizedValidation.valid) {
72
+ return sanitizedValidation.normalized;
73
+ }
74
+ const digits = extractPhoneDigits(sanitized);
75
+ if (!digits) return null;
76
+ if (digits.length >= 9 && digits.length <= 15 && sanitized.startsWith("00")) {
77
+ const internationalCandidate = `+${digits.slice(2)}`;
78
+ const internationalValidation = validatePhoneNumber(internationalCandidate);
79
+ if (internationalValidation.valid) {
80
+ return internationalValidation.normalized;
81
+ }
82
+ }
83
+ const dialCode = resolveCountryDialCode(row);
84
+ if (!dialCode) return null;
85
+ let nationalDigits = digits;
86
+ if (nationalDigits.startsWith(dialCode)) {
87
+ nationalDigits = nationalDigits.slice(dialCode.length);
88
+ } else {
89
+ nationalDigits = nationalDigits.replace(/^0+/, "");
90
+ }
91
+ if (!nationalDigits) return null;
92
+ const localizedCandidate = `+${dialCode}${nationalDigits}`;
93
+ const localizedValidation = validatePhoneNumber(localizedCandidate);
94
+ return localizedValidation.valid ? localizedValidation.normalized : null;
95
+ }
96
+ function normalizeOptionalNumber(value) {
97
+ if (typeof value === "number") {
98
+ return Number.isFinite(value) ? value : null;
99
+ }
100
+ if (typeof value !== "string") return null;
101
+ const trimmed = value.trim();
102
+ if (trimmed.length === 0) return null;
103
+ const parsed = Number(trimmed);
104
+ return Number.isFinite(parsed) ? parsed : null;
105
+ }
106
+ function normalizeCustomFieldDate(value) {
107
+ const parsed = new Date(value);
108
+ if (Number.isNaN(parsed.getTime())) return null;
109
+ return parsed.toISOString().slice(0, 10);
110
+ }
111
+ function normalizeCustomFieldBoolean(value) {
112
+ const normalized = value.trim().toLowerCase();
113
+ if (["true", "1", "yes", "y", "t", "tak"].includes(normalized)) return true;
114
+ if (["false", "0", "no", "n", "f", "nie"].includes(normalized)) return false;
115
+ return null;
116
+ }
117
+ function coerceCustomFieldValue(key, value, definitions) {
118
+ if (typeof value === "string" && value.trim().length === 0) {
119
+ return { ok: true, value: null };
120
+ }
121
+ const definition = definitions.get(key);
122
+ if (!definition) {
123
+ return { ok: true, value: typeof value === "string" ? value.trim() : value };
124
+ }
125
+ if (value === null || value === void 0) {
126
+ return { ok: true, value: null };
127
+ }
128
+ const text = typeof value === "string" ? value.trim() : String(value);
129
+ if (text.length === 0) {
130
+ return { ok: true, value: null };
131
+ }
132
+ const kind = definition.kind.toLowerCase();
133
+ if (kind === "boolean") {
134
+ const booleanValue = normalizeCustomFieldBoolean(text);
135
+ if (booleanValue === null) {
136
+ return { ok: false, error: `Custom field "${key}" expects a boolean value.` };
137
+ }
138
+ return { ok: true, value: booleanValue };
139
+ }
140
+ if (kind === "integer") {
141
+ const numberValue = Number(text);
142
+ if (!Number.isInteger(numberValue)) {
143
+ return { ok: false, error: `Custom field "${key}" expects an integer value.` };
144
+ }
145
+ return { ok: true, value: numberValue };
146
+ }
147
+ if (kind === "float" || kind === "currency" || kind === "number") {
148
+ const numberValue = Number(text);
149
+ if (!Number.isFinite(numberValue)) {
150
+ return { ok: false, error: `Custom field "${key}" expects a number value.` };
151
+ }
152
+ return { ok: true, value: numberValue };
153
+ }
154
+ if (kind === "date" || kind === "datetime") {
155
+ const dateValue = normalizeCustomFieldDate(text);
156
+ if (!dateValue) {
157
+ return { ok: false, error: `Custom field "${key}" expects a date value.` };
158
+ }
159
+ return { ok: true, value: dateValue };
160
+ }
161
+ return { ok: true, value: text };
162
+ }
163
+ function coerceCustomFields(values, definitions) {
164
+ const coerced = {};
165
+ for (const [key, value] of Object.entries(values)) {
166
+ const result = coerceCustomFieldValue(key, value, definitions);
167
+ if (!result.ok) return result;
168
+ coerced[key] = result.value;
169
+ }
170
+ return { ok: true, values: coerced };
171
+ }
172
+ function customFieldDefinitionScopeScore(definition) {
173
+ return (definition.tenantId ? 2 : 0) + (definition.organizationId ? 1 : 0);
174
+ }
175
+ function customFieldDefinitionUpdatedAt(definition) {
176
+ if (!definition.updatedAt) return 0;
177
+ const value = definition.updatedAt instanceof Date ? definition.updatedAt.getTime() : new Date(definition.updatedAt).getTime();
178
+ return Number.isFinite(value) ? value : 0;
179
+ }
180
+ function preferCustomFieldDefinition(current, candidate) {
181
+ if (!current) return true;
182
+ const candidateScore = customFieldDefinitionScopeScore(candidate);
183
+ const currentScore = customFieldDefinitionScopeScore(current);
184
+ if (candidateScore !== currentScore) return candidateScore > currentScore;
185
+ return customFieldDefinitionUpdatedAt(candidate) >= customFieldDefinitionUpdatedAt(current);
186
+ }
187
+ async function loadImportCustomFieldDefinitions(em, scope) {
188
+ const definitions = await findWithDecryption(
189
+ em,
190
+ CustomFieldDef,
191
+ {
192
+ entityId: { $in: [E.customers.customer_entity, E.customers.customer_person_profile] },
193
+ deletedAt: null,
194
+ isActive: true,
195
+ $and: [
196
+ { $or: [{ tenantId: scope.tenantId }, { tenantId: null }] },
197
+ { $or: [{ organizationId: scope.organizationId }, { organizationId: null }] }
198
+ ]
199
+ },
200
+ void 0,
201
+ scope
202
+ );
203
+ const byKey = /* @__PURE__ */ new Map();
204
+ for (const definition of definitions) {
205
+ const candidate = {
206
+ key: definition.key,
207
+ kind: definition.kind,
208
+ entityId: definition.entityId,
209
+ organizationId: definition.organizationId ?? null,
210
+ tenantId: definition.tenantId ?? null,
211
+ updatedAt: definition.updatedAt ?? null
212
+ };
213
+ if (preferCustomFieldDefinition(byKey.get(candidate.key), candidate)) {
214
+ byKey.set(candidate.key, candidate);
215
+ }
216
+ }
217
+ return byKey;
218
+ }
219
+ function buildCommandContext(container, scope) {
220
+ return {
221
+ container,
222
+ auth: null,
223
+ organizationScope: {
224
+ selectedId: scope.organizationId,
225
+ filterIds: [scope.organizationId],
226
+ allowedIds: [scope.organizationId],
227
+ tenantId: scope.tenantId
228
+ },
229
+ selectedOrganizationId: scope.organizationId,
230
+ organizationIds: [scope.organizationId]
231
+ };
232
+ }
233
+ function createCursor(uploadId, offset) {
234
+ return JSON.stringify({
235
+ uploadId,
236
+ offset
237
+ });
238
+ }
239
+ function parseCursor(value) {
240
+ if (!value) return null;
241
+ try {
242
+ const parsed = JSON.parse(value);
243
+ if (!parsed || typeof parsed !== "object") return null;
244
+ if (typeof parsed.uploadId !== "string" || parsed.uploadId.trim().length === 0) return null;
245
+ if (typeof parsed.offset !== "number" || !Number.isFinite(parsed.offset) || parsed.offset < 0) return null;
246
+ return {
247
+ uploadId: parsed.uploadId,
248
+ offset: parsed.offset
249
+ };
250
+ } catch {
251
+ return null;
252
+ }
253
+ }
254
+ function mapRowValues(row, fields) {
255
+ const values = {};
256
+ const customFields = {};
257
+ const addressValues = {};
258
+ for (const field of fields) {
259
+ if (field.mappingKind === "ignore") continue;
260
+ const rawValue = row[field.externalField];
261
+ if (rawValue === void 0 || rawValue === null) continue;
262
+ if (field.mappingKind === "custom_field" || field.localField.startsWith("cf:")) {
263
+ const customFieldKey = field.localField.startsWith("cf:") ? field.localField.slice(3) : field.localField;
264
+ if (customFieldKey.trim().length > 0) {
265
+ customFields[customFieldKey] = rawValue;
266
+ }
267
+ continue;
268
+ }
269
+ if (field.localField === "person.externalId") {
270
+ values.externalId = normalizeOptionalString(rawValue);
271
+ continue;
272
+ }
273
+ if (field.localField === "person.firstName") {
274
+ values.firstName = normalizeOptionalString(rawValue);
275
+ continue;
276
+ }
277
+ if (field.localField === "person.lastName") {
278
+ values.lastName = normalizeOptionalString(rawValue);
279
+ continue;
280
+ }
281
+ if (field.localField === "person.displayName") {
282
+ values.displayName = normalizeOptionalString(rawValue);
283
+ continue;
284
+ }
285
+ if (field.localField === "person.primaryEmail") {
286
+ values.primaryEmail = normalizeEmail(rawValue);
287
+ continue;
288
+ }
289
+ if (field.localField === "person.primaryPhone") {
290
+ values.primaryPhone = normalizeImportedPhone(rawValue, row);
291
+ continue;
292
+ }
293
+ if (field.localField === "person.jobTitle") {
294
+ values.jobTitle = normalizeOptionalString(rawValue);
295
+ continue;
296
+ }
297
+ if (field.localField === "person.status") {
298
+ values.status = normalizeOptionalString(rawValue);
299
+ continue;
300
+ }
301
+ if (field.localField === "person.source") {
302
+ values.source = normalizeOptionalString(rawValue);
303
+ continue;
304
+ }
305
+ if (field.localField === "person.description") {
306
+ values.description = normalizeOptionalString(rawValue);
307
+ continue;
308
+ }
309
+ if (field.localField === "address.name") {
310
+ addressValues.name = normalizeOptionalString(rawValue);
311
+ continue;
312
+ }
313
+ if (field.localField === "address.purpose") {
314
+ addressValues.purpose = normalizeOptionalString(rawValue);
315
+ continue;
316
+ }
317
+ if (field.localField === "address.companyName") {
318
+ addressValues.companyName = normalizeOptionalString(rawValue);
319
+ continue;
320
+ }
321
+ if (field.localField === "address.addressLine1") {
322
+ addressValues.addressLine1 = normalizeOptionalString(rawValue);
323
+ continue;
324
+ }
325
+ if (field.localField === "address.addressLine2") {
326
+ addressValues.addressLine2 = normalizeOptionalString(rawValue);
327
+ continue;
328
+ }
329
+ if (field.localField === "address.buildingNumber") {
330
+ addressValues.buildingNumber = normalizeOptionalString(rawValue);
331
+ continue;
332
+ }
333
+ if (field.localField === "address.flatNumber") {
334
+ addressValues.flatNumber = normalizeOptionalString(rawValue);
335
+ continue;
336
+ }
337
+ if (field.localField === "address.city") {
338
+ addressValues.city = normalizeOptionalString(rawValue);
339
+ continue;
340
+ }
341
+ if (field.localField === "address.region") {
342
+ addressValues.region = normalizeOptionalString(rawValue);
343
+ continue;
344
+ }
345
+ if (field.localField === "address.postalCode") {
346
+ addressValues.postalCode = normalizeOptionalString(rawValue);
347
+ continue;
348
+ }
349
+ if (field.localField === "address.country") {
350
+ addressValues.country = normalizeOptionalString(rawValue);
351
+ continue;
352
+ }
353
+ if (field.localField === "address.latitude") {
354
+ addressValues.latitude = normalizeOptionalNumber(rawValue);
355
+ continue;
356
+ }
357
+ if (field.localField === "address.longitude") {
358
+ addressValues.longitude = normalizeOptionalNumber(rawValue);
359
+ }
360
+ }
361
+ return {
362
+ values,
363
+ customFields,
364
+ addressValues
365
+ };
366
+ }
367
+ function derivePersonNames(values) {
368
+ const firstName = values.firstName ?? null;
369
+ const lastName = values.lastName ?? null;
370
+ const explicitDisplayName = values.displayName ?? null;
371
+ if (firstName && lastName) {
372
+ return {
373
+ firstName,
374
+ lastName,
375
+ displayName: explicitDisplayName ?? `${firstName} ${lastName}`.trim()
376
+ };
377
+ }
378
+ if (explicitDisplayName) {
379
+ const parts = explicitDisplayName.split(/\s+/).filter((part) => part.length > 0);
380
+ if (parts.length >= 2) {
381
+ return {
382
+ firstName: firstName ?? parts.slice(0, -1).join(" "),
383
+ lastName: lastName ?? parts.at(-1) ?? explicitDisplayName,
384
+ displayName: explicitDisplayName
385
+ };
386
+ }
387
+ return {
388
+ firstName: firstName ?? explicitDisplayName,
389
+ lastName: lastName ?? explicitDisplayName,
390
+ displayName: explicitDisplayName
391
+ };
392
+ }
393
+ return null;
394
+ }
395
+ function buildPersonPayload(row, mapping, scope) {
396
+ const { values, customFields, addressValues } = mapRowValues(row, mapping.fields);
397
+ const derivedNames = derivePersonNames(values);
398
+ const sourceIdentifier = values.externalId ?? values.primaryEmail ?? values.displayName ?? null;
399
+ const updateInput = {
400
+ organizationId: scope.organizationId,
401
+ tenantId: scope.tenantId
402
+ };
403
+ if (values.primaryEmail) updateInput.primaryEmail = values.primaryEmail;
404
+ if (values.primaryPhone) updateInput.primaryPhone = values.primaryPhone;
405
+ if (values.jobTitle) updateInput.jobTitle = values.jobTitle;
406
+ if (values.status) updateInput.status = values.status;
407
+ if (values.source) updateInput.source = values.source;
408
+ if (values.description) updateInput.description = values.description;
409
+ if (derivedNames?.firstName) updateInput.firstName = derivedNames.firstName;
410
+ if (derivedNames?.lastName) updateInput.lastName = derivedNames.lastName;
411
+ if (derivedNames?.displayName) updateInput.displayName = derivedNames.displayName;
412
+ if (!derivedNames) {
413
+ return {
414
+ values,
415
+ customFields,
416
+ addressValues,
417
+ createInput: null,
418
+ updateInput,
419
+ sourceIdentifier
420
+ };
421
+ }
422
+ return {
423
+ values,
424
+ customFields,
425
+ addressValues,
426
+ createInput: {
427
+ organizationId: scope.organizationId,
428
+ tenantId: scope.tenantId,
429
+ firstName: derivedNames.firstName,
430
+ lastName: derivedNames.lastName,
431
+ displayName: derivedNames.displayName,
432
+ ...values.primaryEmail ? { primaryEmail: values.primaryEmail } : {},
433
+ ...values.primaryPhone ? { primaryPhone: values.primaryPhone } : {},
434
+ ...values.jobTitle ? { jobTitle: values.jobTitle } : {},
435
+ ...values.status ? { status: values.status } : {},
436
+ ...values.source ? { source: values.source } : {},
437
+ ...values.description ? { description: values.description } : {}
438
+ },
439
+ updateInput,
440
+ sourceIdentifier
441
+ };
442
+ }
443
+ function hasAddressValues(addressValues) {
444
+ return Object.values(addressValues).some((value) => value !== null && value !== void 0);
445
+ }
446
+ function buildAddressCreateInput(addressValues, entityId, scope) {
447
+ if (!addressValues.addressLine1) return null;
448
+ return {
449
+ organizationId: scope.organizationId,
450
+ tenantId: scope.tenantId,
451
+ entityId,
452
+ addressLine1: addressValues.addressLine1,
453
+ isPrimary: true,
454
+ ...addressValues.name ? { name: addressValues.name } : {},
455
+ ...addressValues.purpose ? { purpose: addressValues.purpose } : {},
456
+ ...addressValues.companyName ? { companyName: addressValues.companyName } : {},
457
+ ...addressValues.addressLine2 ? { addressLine2: addressValues.addressLine2 } : {},
458
+ ...addressValues.buildingNumber ? { buildingNumber: addressValues.buildingNumber } : {},
459
+ ...addressValues.flatNumber ? { flatNumber: addressValues.flatNumber } : {},
460
+ ...addressValues.city ? { city: addressValues.city } : {},
461
+ ...addressValues.region ? { region: addressValues.region } : {},
462
+ ...addressValues.postalCode ? { postalCode: addressValues.postalCode } : {},
463
+ ...addressValues.country ? { country: addressValues.country } : {},
464
+ ...addressValues.latitude !== null && addressValues.latitude !== void 0 ? { latitude: addressValues.latitude } : {},
465
+ ...addressValues.longitude !== null && addressValues.longitude !== void 0 ? { longitude: addressValues.longitude } : {}
466
+ };
467
+ }
468
+ async function upsertPrimaryAddress(params) {
469
+ if (!hasAddressValues(params.addressValues)) return;
470
+ if (!params.addressValues.addressLine1) return;
471
+ const [existingPrimaryAddress] = await findWithDecryption(
472
+ params.em,
473
+ CustomerAddress,
474
+ {
475
+ entity: params.entityId,
476
+ organizationId: params.scope.organizationId,
477
+ tenantId: params.scope.tenantId,
478
+ isPrimary: true
479
+ },
480
+ {
481
+ orderBy: {
482
+ createdAt: "asc"
483
+ },
484
+ limit: 1
485
+ },
486
+ params.scope
487
+ );
488
+ if (existingPrimaryAddress) {
489
+ await params.commandBus.execute("customers.addresses.update", {
490
+ input: {
491
+ id: existingPrimaryAddress.id,
492
+ isPrimary: true,
493
+ ...params.addressValues.name ? { name: params.addressValues.name } : {},
494
+ ...params.addressValues.purpose ? { purpose: params.addressValues.purpose } : {},
495
+ ...params.addressValues.companyName ? { companyName: params.addressValues.companyName } : {},
496
+ ...params.addressValues.addressLine1 ? { addressLine1: params.addressValues.addressLine1 } : {},
497
+ ...params.addressValues.addressLine2 ? { addressLine2: params.addressValues.addressLine2 } : {},
498
+ ...params.addressValues.buildingNumber ? { buildingNumber: params.addressValues.buildingNumber } : {},
499
+ ...params.addressValues.flatNumber ? { flatNumber: params.addressValues.flatNumber } : {},
500
+ ...params.addressValues.city ? { city: params.addressValues.city } : {},
501
+ ...params.addressValues.region ? { region: params.addressValues.region } : {},
502
+ ...params.addressValues.postalCode ? { postalCode: params.addressValues.postalCode } : {},
503
+ ...params.addressValues.country ? { country: params.addressValues.country } : {},
504
+ ...params.addressValues.latitude !== null && params.addressValues.latitude !== void 0 ? { latitude: params.addressValues.latitude } : {},
505
+ ...params.addressValues.longitude !== null && params.addressValues.longitude !== void 0 ? { longitude: params.addressValues.longitude } : {}
506
+ },
507
+ ctx: params.commandContext
508
+ });
509
+ return;
510
+ }
511
+ const createInput = buildAddressCreateInput(params.addressValues, params.entityId, params.scope);
512
+ if (!createInput) return;
513
+ await params.commandBus.execute("customers.addresses.create", {
514
+ input: createInput,
515
+ ctx: params.commandContext
516
+ });
517
+ }
518
+ async function loadStoredMapping(em, entityType, scope) {
519
+ const stored = await findOneWithDecryption(
520
+ em,
521
+ SyncMapping,
522
+ {
523
+ integrationId: "sync_excel",
524
+ entityType,
525
+ organizationId: scope.organizationId,
526
+ tenantId: scope.tenantId
527
+ },
528
+ void 0,
529
+ scope
530
+ );
531
+ if (stored?.mapping && typeof stored.mapping === "object") {
532
+ return stored.mapping;
533
+ }
534
+ return {
535
+ entityType,
536
+ fields: [],
537
+ matchStrategy: "custom"
538
+ };
539
+ }
540
+ async function resolveUpload(em, runId, cursor, scope) {
541
+ if (runId) {
542
+ const uploadByRun = await findOneWithDecryption(
543
+ em,
544
+ SyncExcelUpload,
545
+ {
546
+ syncRunId: runId,
547
+ organizationId: scope.organizationId,
548
+ tenantId: scope.tenantId
549
+ },
550
+ void 0,
551
+ scope
552
+ );
553
+ if (uploadByRun) return uploadByRun;
554
+ }
555
+ if (cursor?.uploadId) {
556
+ return findOneWithDecryption(
557
+ em,
558
+ SyncExcelUpload,
559
+ {
560
+ id: cursor.uploadId,
561
+ organizationId: scope.organizationId,
562
+ tenantId: scope.tenantId
563
+ },
564
+ void 0,
565
+ scope
566
+ );
567
+ }
568
+ return null;
569
+ }
570
+ async function resolveExistingPersonId(params) {
571
+ if (params.externalId) {
572
+ const mappedLocalId = await params.externalIdMappingService.lookupLocalId(
573
+ "sync_excel",
574
+ "customers.person",
575
+ params.externalId,
576
+ params.scope
577
+ );
578
+ if (mappedLocalId) return mappedLocalId;
579
+ }
580
+ if (!params.email) return null;
581
+ return params.emailDedupeIndex.get(params.email) ?? null;
582
+ }
583
+ async function buildEmailDedupeIndex(params) {
584
+ const emails = /* @__PURE__ */ new Set();
585
+ for (const row of params.rows) {
586
+ const rowValues = mapRowValues(row, params.mapping.fields);
587
+ const email = rowValues.values.primaryEmail;
588
+ if (email) emails.add(email);
589
+ }
590
+ if (emails.size === 0) return /* @__PURE__ */ new Map();
591
+ const candidates = await findWithDecryption(
592
+ params.em,
593
+ CustomerEntity,
594
+ {
595
+ kind: "person",
596
+ organizationId: params.scope.organizationId,
597
+ tenantId: params.scope.tenantId,
598
+ deletedAt: null,
599
+ isActive: true
600
+ },
601
+ {
602
+ orderBy: {
603
+ createdAt: "asc"
604
+ }
605
+ },
606
+ params.scope
607
+ );
608
+ const index = /* @__PURE__ */ new Map();
609
+ for (const candidate of candidates) {
610
+ const email = normalizeEmail(candidate.primaryEmail);
611
+ if (!email || !emails.has(email) || index.has(email)) continue;
612
+ index.set(email, candidate.id);
613
+ }
614
+ return index;
615
+ }
616
+ function isEmptyRow(row) {
617
+ return !Object.values(row).some((value) => normalizeOptionalString(value));
618
+ }
619
+ async function processRow(params) {
620
+ if (isEmptyRow(params.row)) {
621
+ return {
622
+ externalId: `row:${params.rowNumber}`,
623
+ action: "skip",
624
+ data: {
625
+ rowNumber: params.rowNumber,
626
+ reason: "empty_row"
627
+ }
628
+ };
629
+ }
630
+ const payload = buildPersonPayload(params.row, params.mapping, params.scope);
631
+ const externalId = payload.values.externalId ?? null;
632
+ const sourceIdentifier = payload.sourceIdentifier ?? `row:${params.rowNumber}`;
633
+ const customFieldResult = coerceCustomFields(payload.customFields, params.customFieldDefinitions);
634
+ if (!customFieldResult.ok) {
635
+ return {
636
+ externalId: externalId ?? sourceIdentifier,
637
+ action: "failed",
638
+ data: {
639
+ rowNumber: params.rowNumber,
640
+ sourceIdentifier,
641
+ errorMessage: customFieldResult.error
642
+ }
643
+ };
644
+ }
645
+ const existingId = await resolveExistingPersonId({
646
+ externalIdMappingService: params.externalIdMappingService,
647
+ externalId,
648
+ email: payload.values.primaryEmail,
649
+ emailDedupeIndex: params.emailDedupeIndex,
650
+ scope: params.scope
651
+ });
652
+ if (!existingId && !payload.createInput) {
653
+ return {
654
+ externalId: externalId ?? sourceIdentifier,
655
+ action: "failed",
656
+ data: {
657
+ rowNumber: params.rowNumber,
658
+ sourceIdentifier,
659
+ errorMessage: "Import row is missing a usable person name mapping."
660
+ }
661
+ };
662
+ }
663
+ try {
664
+ if (existingId) {
665
+ const updateInput = {
666
+ id: existingId,
667
+ ...payload.updateInput,
668
+ ...Object.keys(customFieldResult.values).length > 0 ? { customFields: customFieldResult.values } : {}
669
+ };
670
+ await params.commandBus.execute("customers.people.update", {
671
+ input: updateInput,
672
+ ctx: params.commandContext
673
+ });
674
+ await upsertPrimaryAddress({
675
+ entityId: existingId,
676
+ addressValues: payload.addressValues,
677
+ scope: params.scope,
678
+ commandBus: params.commandBus,
679
+ commandContext: params.commandContext,
680
+ em: params.em
681
+ });
682
+ if (externalId) {
683
+ await params.externalIdMappingService.storeExternalIdMapping(
684
+ "sync_excel",
685
+ "customers.person",
686
+ existingId,
687
+ externalId,
688
+ params.scope
689
+ );
690
+ }
691
+ if (payload.values.primaryEmail && !params.emailDedupeIndex.has(payload.values.primaryEmail)) {
692
+ params.emailDedupeIndex.set(payload.values.primaryEmail, existingId);
693
+ }
694
+ return {
695
+ externalId: externalId ?? sourceIdentifier,
696
+ action: "update",
697
+ data: {
698
+ localId: existingId,
699
+ rowNumber: params.rowNumber,
700
+ sourceIdentifier
701
+ }
702
+ };
703
+ }
704
+ const commandResult = await params.commandBus.execute("customers.people.create", {
705
+ input: {
706
+ ...payload.createInput,
707
+ ...Object.keys(customFieldResult.values).length > 0 ? { customFields: customFieldResult.values } : {}
708
+ },
709
+ ctx: params.commandContext
710
+ });
711
+ await upsertPrimaryAddress({
712
+ entityId: commandResult.result.entityId,
713
+ addressValues: payload.addressValues,
714
+ scope: params.scope,
715
+ commandBus: params.commandBus,
716
+ commandContext: params.commandContext,
717
+ em: params.em
718
+ });
719
+ if (externalId) {
720
+ await params.externalIdMappingService.storeExternalIdMapping(
721
+ "sync_excel",
722
+ "customers.person",
723
+ commandResult.result.entityId,
724
+ externalId,
725
+ params.scope
726
+ );
727
+ }
728
+ if (payload.values.primaryEmail && !params.emailDedupeIndex.has(payload.values.primaryEmail)) {
729
+ params.emailDedupeIndex.set(payload.values.primaryEmail, commandResult.result.entityId);
730
+ }
731
+ return {
732
+ externalId: externalId ?? sourceIdentifier,
733
+ action: "create",
734
+ data: {
735
+ localId: commandResult.result.entityId,
736
+ personId: commandResult.result.personId,
737
+ rowNumber: params.rowNumber,
738
+ sourceIdentifier
739
+ }
740
+ };
741
+ } catch (error) {
742
+ const errorMessage = error instanceof Error ? error.message : "Import row failed";
743
+ return {
744
+ externalId: externalId ?? sourceIdentifier,
745
+ action: "failed",
746
+ data: {
747
+ rowNumber: params.rowNumber,
748
+ sourceIdentifier,
749
+ errorMessage
750
+ }
751
+ };
752
+ }
753
+ }
754
+ const syncExcelCustomersAdapter = {
755
+ providerKey: "excel",
756
+ operationalTelemetry: true,
757
+ direction: "import",
758
+ supportedEntities: ["customers.person"],
759
+ async getMapping(input) {
760
+ const container = await createRequestContainer();
761
+ const em = container.resolve("em");
762
+ return loadStoredMapping(em, input.entityType, input.scope);
763
+ },
764
+ async *streamImport(input) {
765
+ if (input.entityType !== "customers.person") {
766
+ throw new Error(`Unsupported sync_excel entity type: ${input.entityType}`);
767
+ }
768
+ const container = await createRequestContainer();
769
+ const em = container.resolve("em");
770
+ const commandBus = container.resolve("commandBus");
771
+ const externalIdMappingService = container.resolve("externalIdMappingService");
772
+ const cursor = parseCursor(input.cursor);
773
+ const upload = await resolveUpload(em, input.runId, cursor, input.scope);
774
+ if (!upload) {
775
+ throw new Error("CSV upload session was not found for this sync run.");
776
+ }
777
+ if (input.runId) {
778
+ upload.syncRunId = input.runId;
779
+ }
780
+ upload.status = "importing";
781
+ await em.flush();
782
+ try {
783
+ const attachment = await findOneWithDecryption(
784
+ em,
785
+ Attachment,
786
+ {
787
+ id: upload.attachmentId,
788
+ organizationId: input.scope.organizationId,
789
+ tenantId: input.scope.tenantId
790
+ },
791
+ void 0,
792
+ input.scope
793
+ );
794
+ if (!attachment) {
795
+ throw new Error("CSV upload attachment could not be found.");
796
+ }
797
+ const fileBuffer = await readSyncExcelUploadBuffer(attachment);
798
+ const document = parseCsvDocument(fileBuffer);
799
+ const startOffset = cursor?.uploadId === upload.id ? cursor.offset : 0;
800
+ const commandContext = buildCommandContext(container, input.scope);
801
+ const customFieldDefinitions = await loadImportCustomFieldDefinitions(em, input.scope);
802
+ for (let offset = startOffset, batchIndex = 0; offset < document.rows.length; offset += input.batchSize, batchIndex += 1) {
803
+ const batchRows = document.rows.slice(offset, offset + input.batchSize);
804
+ const emailDedupeIndex = await buildEmailDedupeIndex({
805
+ em,
806
+ rows: batchRows,
807
+ mapping: input.mapping,
808
+ scope: input.scope
809
+ });
810
+ const items = [];
811
+ for (let index = 0; index < batchRows.length; index += 1) {
812
+ items.push(await processRow({
813
+ row: batchRows[index],
814
+ rowNumber: offset + index + 1,
815
+ mapping: input.mapping,
816
+ scope: input.scope,
817
+ commandBus,
818
+ commandContext,
819
+ externalIdMappingService,
820
+ emailDedupeIndex,
821
+ customFieldDefinitions,
822
+ em
823
+ }));
824
+ }
825
+ const nextOffset = offset + batchRows.length;
826
+ yield {
827
+ items,
828
+ cursor: createCursor(upload.id, nextOffset),
829
+ hasMore: nextOffset < document.rows.length,
830
+ totalEstimate: document.totalRows,
831
+ processedCount: batchRows.length,
832
+ refreshCoverageEntityTypes: CUSTOMER_IMPORT_COVERAGE_ENTITY_TYPES,
833
+ batchIndex,
834
+ message: `Processed ${nextOffset} of ${document.totalRows} CSV rows`
835
+ };
836
+ }
837
+ upload.status = "completed";
838
+ await em.flush();
839
+ } catch (error) {
840
+ upload.status = "failed";
841
+ await em.flush();
842
+ throw error;
843
+ }
844
+ }
845
+ };
846
+ export {
847
+ buildPersonPayload,
848
+ createCursor,
849
+ parseCursor,
850
+ syncExcelCustomersAdapter
851
+ };
852
+ //# sourceMappingURL=customers.js.map