thinkwork-cli 0.13.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -14
- package/dist/{api-client-4DZ266J5.js → api-client-4P4JZOBX.js} +3 -1
- package/dist/bowser-6Z6JFAIR.js +2822 -0
- package/dist/{chunk-Y2WHJECR.js → chunk-4OMDWYZQ.js} +5 -44
- package/dist/chunk-AD2NIWB5.js +6516 -0
- package/dist/chunk-DV3MRBR5.js +43 -0
- package/dist/chunk-EI5ZX7CN.js +522 -0
- package/dist/chunk-EUJ7X6YD.js +126 -0
- package/dist/chunk-GG5WKBOX.js +436 -0
- package/dist/chunk-I64EZSU5.js +358 -0
- package/dist/chunk-KMIJ5KGT.js +128 -0
- package/dist/chunk-MWQ7RO76.js +137 -0
- package/dist/chunk-OCPHU34C.js +996 -0
- package/dist/chunk-OLUQ4U2Z.js +1728 -0
- package/dist/chunk-XYM7PE2A.js +2264 -0
- package/dist/chunk-YQCYD6NE.js +3299 -0
- package/dist/cli.js +49156 -31257
- package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +0 -1
- package/dist/commands/enterprise/templates/deploy-repo/scripts/smoke.mjs +0 -1
- package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +0 -14
- package/dist/dist-es-2OTNTL4N.js +315 -0
- package/dist/dist-es-2XVAH4BQ.js +14726 -0
- package/dist/dist-es-3DPDUJAH.js +2000 -0
- package/dist/dist-es-BNZTBBVC.js +176 -0
- package/dist/dist-es-CD2RBDAC.js +88 -0
- package/dist/dist-es-I4FW57PS.js +88 -0
- package/dist/dist-es-KL7Y5NB2.js +377 -0
- package/dist/dist-es-KVFWJNDY.js +318 -0
- package/dist/dist-es-PS73DWNL.js +70 -0
- package/dist/dist-es-S3NZQFGT.js +170 -0
- package/dist/dist-es-SGCA4P32.js +48 -0
- package/dist/dist-es-WBM6J4RA.js +70 -0
- package/dist/dist-es-ZGVADYIF.js +485 -0
- package/dist/dist-es-ZKC2QKD2.js +377 -0
- package/dist/drizzle/0076_scheduled_jobs_marco_backfill.sql +2 -1
- package/dist/drizzle/0079_seed_tenant_customize_catalog.sql +1 -0
- package/dist/drizzle/0090_brain_schema_extraction.sql +10 -0
- package/dist/drizzle/0132_drop_computer_tables.sql +3 -0
- package/dist/drizzle/0160_compliance_event_types_plugins.sql +2 -0
- package/dist/drizzle/0214_think137_drop_judge_evidence_roi.sql +1 -0
- package/dist/drizzle/0216_drop_agent_loop_versions_goal_worker_policy.sql +1 -0
- package/dist/drizzle/0227_analyst_reader_role.sql +17 -20
- package/dist/drizzle/0230_analyst_rls.sql +28 -36
- package/dist/drizzle/0250_kg_schema_extraction.sql +27 -0
- package/dist/drizzle/0251_brain_stragglers.sql +21 -0
- package/dist/drizzle/0253_drop_brain_substrate.sql +2 -0
- package/dist/drizzle/0257_agents_runtime_allow_harness.sql +24 -0
- package/dist/drizzle/0261_native_auth_control_plane.sql +332 -0
- package/dist/drizzle/0262_auth_subscription_tickets.sql +59 -0
- package/dist/drizzle/0263_agentcore_runtime_identifier.sql +69 -0
- package/dist/drizzle/0263_drop_workos_auth_runtime.sql +236 -0
- package/dist/drizzle/0264_auth_enrollment_attempt_limits.sql +15 -0
- package/dist/drizzle/0265_eval_runs_requester_identity.sql +28 -0
- package/dist/drizzle/0265_require_fully_valid_auth_publication.sql +27 -0
- package/dist/drizzle/0266_auth_identity_recovery_grants.sql +9 -0
- package/dist/drizzle/0266_eval_profiles_runtime_type.sql +12 -0
- package/dist/drizzle/0267_ontology_candidate_rejections.sql +53 -0
- package/dist/drizzle/0268_identity_crosswalk_routing.sql +182 -0
- package/dist/drizzle/0269_ontology_system_map.sql +31 -0
- package/dist/drizzle/0270_tool_execution_ledger.sql +115 -0
- package/dist/drizzle/0271_ontology_twin_declarations.sql +47 -0
- package/dist/drizzle/0272_identity_graph_projection_cursor.sql +23 -0
- package/dist/drizzle/0273_twin_materialization_suggestions.sql +29 -0
- package/dist/drizzle/0274_tenant_mcp_twin_keys.sql +59 -0
- package/dist/drizzle/0275_identity_bulk_rebuild_fence.sql +30 -0
- package/dist/drizzle/0276_tool_execution_ledger_allow_cascade_delete.sql +18 -0
- package/dist/drizzle/0277_knowledge_base_sources.sql +107 -0
- package/dist/drizzle/0278_kb_page_transcription.sql +55 -0
- package/dist/drizzle/0279_mcp_servers_off_plugin_provenance.sql +93 -0
- package/dist/drizzle/0280_work_item_external_refs_drop_plane_provider.sql +46 -0
- package/dist/drizzle/0281_tenant_mcp_twin_keys_expiry_suffix.sql +9 -0
- package/dist/drizzle/0282_tenant_mcp_twin_keys_grants.sql +24 -0
- package/dist/drizzle/0283_agents_agentcore_runtime_dispatch.sql +13 -0
- package/dist/drizzle/0284_user_brain_claims.sql +69 -0
- package/dist/drizzle/0285_tenant_mcp_twin_keys_trusted_subsystem.sql +30 -0
- package/dist/drizzle/0286_brain_analytics_key.sql +26 -0
- package/dist/drizzle/0287_brain_subject.sql +25 -0
- package/dist/drizzle/0288_multi_lane_identities.sql +25 -0
- package/dist/event-streams-7JNLJEEL.js +894 -0
- package/dist/event-streams-KDF5RWR6.js +1515 -0
- package/dist/loadSso-ETCXTR4I.js +576 -0
- package/dist/loadSso-UBD7KUHE.js +583 -0
- package/dist/plugins/README.md +2 -2
- package/dist/plugins/catalog/package.json +1 -5
- package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +6 -4
- package/dist/plugins/catalog/src/__tests__/catalog.test.ts +21 -21
- package/dist/plugins/catalog/src/__tests__/contracts.test.ts +102 -94
- package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +7 -8
- package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +0 -20
- package/dist/plugins/catalog/src/contracts.ts +17 -179
- package/dist/plugins/catalog/src/registry/generated-first-party.ts +0 -24
- package/dist/plugins/n8n/test/integrated-app-contract.test.ts +1 -1
- package/dist/plugins/twenty/package.json +2 -1
- package/dist/plugins/twenty/scripts/LAMBDA_DEPLOY.md +114 -0
- package/dist/plugins/twenty/scripts/build-lambda.sh +29 -0
- package/dist/plugins/twenty/scripts/configure-product-views.ts +195 -0
- package/dist/plugins/twenty/scripts/lambda-handler.ts +49 -0
- package/dist/plugins/twenty/scripts/lib/__tests__/load-attachments.test.ts +38 -0
- package/dist/plugins/twenty/scripts/lib/__tests__/load-records.test.ts +145 -2
- package/dist/plugins/twenty/scripts/lib/__tests__/mappers.test.ts +397 -151
- package/dist/plugins/twenty/scripts/lib/__tests__/members-ensure.test.ts +28 -129
- package/dist/plugins/twenty/scripts/lib/__tests__/provision-members-db.test.ts +10 -0
- package/dist/plugins/twenty/scripts/lib/__tests__/schema-ensure.test.ts +13 -8
- package/dist/plugins/twenty/scripts/lib/lastmile-reader.ts +222 -2
- package/dist/plugins/twenty/scripts/lib/load-attachments.ts +29 -0
- package/dist/plugins/twenty/scripts/lib/load-records.ts +160 -43
- package/dist/plugins/twenty/scripts/lib/mappers.ts +368 -152
- package/dist/plugins/twenty/scripts/lib/members-ensure.ts +43 -268
- package/dist/plugins/twenty/scripts/lib/provision-members-db.ts +57 -0
- package/dist/plugins/twenty/scripts/lib/schema-ensure.ts +136 -31
- package/dist/plugins/twenty/scripts/migrate-lastmile.ts +254 -152
- package/dist/plugins/twenty/scripts/provision-twenty-members.ts +44 -1
- package/dist/plugins/twenty/scripts/purge-lastmile-import.ts +178 -0
- package/dist/plugins/twenty/scripts/restructure-products.ts +312 -0
- package/dist/plugins/twenty/scripts/retag-stage-values.ts +362 -0
- package/dist/plugins/twenty/test/manifest.test.ts +2 -4
- package/dist/scripts/post-deploy.sh +79 -1
- package/dist/signin-5QZNRSOO.js +821 -0
- package/dist/sso-oidc-XK673P54.js +837 -0
- package/dist/sts-OXI2UALF.js +1113 -0
- package/dist/sts-SQW4NBK3.js +1117 -0
- package/dist/terraform/examples/greenfield/main.tf +391 -176
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +26 -8
- package/dist/terraform/modules/app/agentcore-admin/main.tf +47 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +39 -47
- package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +13 -66
- package/dist/terraform/modules/app/agentcore-identity/main.tf +253 -0
- package/dist/terraform/modules/app/agentcore-identity/outputs.tf +51 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/bootstrap_twenty_oauth_client.sh +239 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/delete_identity.sh +22 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/delete_twenty_identity.sh +16 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/delete_workload_identity.sh +19 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/ensure_workload_identity.sh +29 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/read_identity.sh +55 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_identity.sh +73 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_identity.sh +45 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_provider.mjs +123 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/after.tf +15 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/before.tf +7 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-after.tf +8 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-before.tf +9 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-after.tf +3 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-before.tf +3 -0
- package/dist/terraform/modules/app/agentcore-identity/variables.tf +60 -0
- package/dist/terraform/modules/app/agentcore-memory/README.md +46 -9
- package/dist/terraform/modules/app/agentcore-memory/main.tf +27 -10
- package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +156 -24
- package/dist/terraform/modules/app/agentcore-pi/README.md +1 -3
- package/dist/terraform/modules/app/agentcore-pi/main.tf +42 -52
- package/dist/terraform/modules/app/agentcore-pi/outputs.tf +3 -3
- package/dist/terraform/modules/app/agentcore-pi/scripts/reconcile_pi_runtime.mjs +175 -0
- package/dist/terraform/modules/app/agentcore-pi/variables.tf +35 -28
- package/dist/terraform/modules/app/agentcore-platform/main.tf +68 -0
- package/dist/terraform/modules/app/agentcore-runtime/main.tf +4 -20
- package/dist/terraform/modules/app/appsync-subscriptions/main.tf +127 -21
- package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +0 -6
- package/dist/terraform/modules/app/appsync-subscriptions/variables.tf +5 -0
- package/dist/terraform/modules/app/capability-broker/main.tf +2 -2
- package/dist/terraform/modules/app/capability-broker/variables.tf +11 -0
- package/dist/terraform/modules/app/deployment-control-plane/README.md +264 -0
- package/dist/terraform/modules/app/deployment-control-plane/buildspec.yml +4 -1
- package/dist/terraform/modules/app/deployment-control-plane/main.tf +94 -0
- package/dist/terraform/modules/app/deployment-control-plane/outputs.tf +5 -0
- package/dist/terraform/modules/app/deployment-control-plane/runner.py +3689 -233
- package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +2579 -140
- package/dist/terraform/modules/app/deployment-control-plane/variables.tf +33 -0
- package/dist/terraform/modules/app/lambda-api/auth-state.tf +72 -0
- package/dist/terraform/modules/app/lambda-api/chat-latency-observability.tf +466 -0
- package/dist/terraform/modules/app/lambda-api/dispatch-dlq.tf +55 -0
- package/dist/terraform/modules/app/lambda-api/handlers.tf +531 -619
- package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +350 -241
- package/dist/terraform/modules/app/lambda-api/main.tf +75 -13
- package/dist/terraform/modules/app/lambda-api/memory-alarms.tf +3 -28
- package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +1 -2
- package/dist/terraform/modules/app/lambda-api/outputs.tf +17 -10
- package/dist/terraform/modules/app/lambda-api/runtime-config.tf +3 -21
- package/dist/terraform/modules/app/lambda-api/turn-assertion.tf +45 -0
- package/dist/terraform/modules/app/lambda-api/variables.tf +307 -121
- package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/README.md +1 -1
- package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/main.tf +18 -18
- package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +11 -10
- package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +5 -5
- package/dist/terraform/modules/app/static-site/main.tf +1 -1
- package/dist/terraform/modules/app/www-dns/main.tf +9 -27
- package/dist/terraform/modules/app/www-dns/variables.tf +4 -10
- package/dist/terraform/modules/data/aurora-postgres/README.md +34 -0
- package/dist/terraform/modules/data/aurora-postgres/alarms.tf +82 -0
- package/dist/terraform/modules/data/aurora-postgres/main.tf +27 -57
- package/dist/terraform/modules/data/aurora-postgres/outputs.tf +0 -9
- package/dist/terraform/modules/data/aurora-postgres/variables.tf +41 -0
- package/dist/terraform/modules/data/s3-buckets/main.tf +5 -6
- package/dist/terraform/modules/foundation/cognito/main.tf +367 -72
- package/dist/terraform/modules/foundation/cognito/outputs.tf +47 -0
- package/dist/terraform/modules/foundation/cognito/variables.tf +131 -9
- package/dist/terraform/modules/thinkwork/README.md +5 -1
- package/dist/terraform/modules/thinkwork/main.tf +459 -234
- package/dist/terraform/modules/thinkwork/outputs.tf +44 -62
- package/dist/terraform/modules/thinkwork/variables.tf +353 -89
- package/dist/terraform/schema.graphql +69 -36
- package/dist/workspace-defaults/files/INSTRUCTIONS.md +9 -11
- package/dist/workspace-defaults/files/MEMORY_GUIDE.md +38 -35
- package/dist/workspace-defaults/files/ROUTER.md +1 -1
- package/dist/workspace-defaults/files/TOOLS.md +9 -39
- package/dist/workspace-defaults/files/USER.md +1 -0
- package/dist/workspace-defaults/files/agents/analyst/INSTRUCTIONS.md +6 -3
- package/dist/workspace-defaults/files/catalog-skills/brain-brief-builder/SKILL.md +165 -0
- package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/SKILL.md +75 -0
- package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/mcp-tooling.md +40 -0
- package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/validation-and-handoff.md +40 -0
- package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/workflow-authoring.md +46 -0
- package/dist/workspace-defaults/files/skills/artifact-builder/references/crm-dashboard.md +1 -1
- package/dist/workspace-defaults/files/skills/document-composer/SKILL.md +22 -3
- package/package.json +3 -2
- package/dist/drizzle/0261_thread_turns_origin_turn_index.sql +0 -11
- package/dist/plugins/company-data/README.md +0 -49
- package/dist/plugins/company-data/package.json +0 -21
- package/dist/plugins/company-data/src/index.ts +0 -30
- package/dist/plugins/company-data/src/manifest.ts +0 -26
- package/dist/plugins/company-data/test/manifest.test.ts +0 -116
- package/dist/plugins/company-data/tsconfig.json +0 -8
- package/dist/plugins/company-etl/README.md +0 -46
- package/dist/plugins/company-etl/package.json +0 -21
- package/dist/plugins/company-etl/src/index.ts +0 -29
- package/dist/plugins/company-etl/src/manifest.ts +0 -26
- package/dist/plugins/company-etl/test/manifest.test.ts +0 -108
- package/dist/plugins/company-etl/tsconfig.json +0 -8
- package/dist/plugins/lastmile/README.md +0 -50
- package/dist/plugins/lastmile/package.json +0 -23
- package/dist/plugins/lastmile/smoke/lastmile-plugin-smoke.mjs +0 -661
- package/dist/plugins/lastmile/src/api/tasks-adapter.ts +0 -572
- package/dist/plugins/lastmile/src/discovery.fixture.ts +0 -64
- package/dist/plugins/lastmile/src/index.ts +0 -46
- package/dist/plugins/lastmile/src/manifest.ts +0 -139
- package/dist/plugins/lastmile/test/api/tasks-adapter.test.ts +0 -266
- package/dist/plugins/lastmile/test/discovery.test.ts +0 -88
- package/dist/plugins/lastmile/tsconfig.json +0 -8
- package/dist/plugins/n8n/smoke/n8n-managed-app-smoke.mjs +0 -409
- package/dist/plugins/twenty/smoke/twenty-managed-app-smoke.mjs +0 -359
- package/dist/plugins/workos-auth/package.json +0 -22
- package/dist/plugins/workos-auth/src/index.ts +0 -30
- package/dist/plugins/workos-auth/src/manifest.ts +0 -40
- package/dist/plugins/workos-auth/src/provider-contract.ts +0 -39
- package/dist/plugins/workos-auth/test/manifest.test.ts +0 -94
- package/dist/plugins/workos-auth/tsconfig.json +0 -8
- package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +0 -61
- package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/sitecustomize.py +0 -227
- package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/test_sitecustomize.py +0 -277
- package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +0 -70
- package/dist/terraform/modules/app/agentcore-harness/main.tf +0 -101
- package/dist/terraform/modules/app/agentcore-harness/outputs.tf +0 -9
- package/dist/terraform/modules/app/agentcore-harness/variables.tf +0 -25
- package/dist/terraform/modules/app/hindsight-memory/README.md +0 -89
- package/dist/terraform/modules/app/hindsight-memory/main.tf +0 -411
- package/dist/terraform/modules/data/bedrock-knowledge-base/main.tf +0 -102
- /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/outputs.tf +0 -0
- /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/sync-database.py +0 -0
- /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/test_sync_database.py +0 -0
- /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/variables.tf +0 -0
- /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/README.md +0 -0
- /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/main.tf +0 -0
- /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/outputs.tf +0 -0
- /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/variables.tf +0 -0
|
@@ -2,75 +2,43 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
4
|
LastmileContact,
|
|
5
|
+
LastmileCrmTask,
|
|
5
6
|
LastmileCustomerNote,
|
|
6
|
-
LastmileLead,
|
|
7
|
-
LastmileOpportunity,
|
|
8
7
|
} from "../lastmile-reader";
|
|
9
8
|
import {
|
|
10
9
|
buildOwnerIndex,
|
|
11
10
|
contentHash,
|
|
11
|
+
dedupeContactEmails,
|
|
12
12
|
deriveRepEmail,
|
|
13
13
|
isMobilBrand,
|
|
14
14
|
mapAccount,
|
|
15
15
|
mapContact,
|
|
16
|
+
mapDispatchCustomer,
|
|
16
17
|
mapCrmComment,
|
|
18
|
+
mapCrmTask,
|
|
19
|
+
mapOrganization,
|
|
20
|
+
mapProduct,
|
|
21
|
+
normalizeProductName,
|
|
22
|
+
PRODUCT_CATALOG,
|
|
23
|
+
productSourceId,
|
|
24
|
+
mapTaskProducts,
|
|
25
|
+
mapTaskStatusActivity,
|
|
26
|
+
mapTaskStatusToStage,
|
|
17
27
|
mapCustomerNote,
|
|
18
|
-
mapLead,
|
|
19
|
-
mapLeadStatusToStage,
|
|
20
|
-
mapOpportunity,
|
|
21
28
|
mapOpportunityProduct,
|
|
22
|
-
mapOpportunityStage,
|
|
23
29
|
normalizeEmail,
|
|
24
30
|
normalizePhone,
|
|
25
31
|
resolveOwner,
|
|
26
32
|
sourceId,
|
|
27
33
|
stableStringify,
|
|
28
34
|
toAmountMicros,
|
|
35
|
+
toIsoTimestamp,
|
|
29
36
|
toQuantity,
|
|
30
37
|
} from "../mappers";
|
|
31
38
|
|
|
32
39
|
const ownerMap = new Map([["rep_jane", "member-jane-uuid"]]);
|
|
33
40
|
const companyMap = new Map([[sourceId("account", "acct_1"), "company-uuid-1"]]);
|
|
34
41
|
|
|
35
|
-
function lead(overrides: Partial<LastmileLead> = {}): LastmileLead {
|
|
36
|
-
return {
|
|
37
|
-
id: "lead_1",
|
|
38
|
-
status: "00-New",
|
|
39
|
-
companyName: "Acme Fuel",
|
|
40
|
-
firstName: "Ann",
|
|
41
|
-
lastName: "Lee",
|
|
42
|
-
email: null,
|
|
43
|
-
phone: null,
|
|
44
|
-
source: null,
|
|
45
|
-
description: null,
|
|
46
|
-
ownerRepId: null,
|
|
47
|
-
dateCreated: null,
|
|
48
|
-
...overrides,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function opportunity(
|
|
53
|
-
overrides: Partial<LastmileOpportunity> = {},
|
|
54
|
-
): LastmileOpportunity {
|
|
55
|
-
return {
|
|
56
|
-
id: "opp_1",
|
|
57
|
-
name: "Bulk diesel contract",
|
|
58
|
-
stage: "10-Prospect",
|
|
59
|
-
amount: "1234.56",
|
|
60
|
-
quantity: "100",
|
|
61
|
-
productType: "Diesel",
|
|
62
|
-
brand: "Mobil",
|
|
63
|
-
closed: null,
|
|
64
|
-
won: null,
|
|
65
|
-
accountId: "acct_1",
|
|
66
|
-
ownerRepId: "rep_jane",
|
|
67
|
-
expectedCloseDate: "2026-08-01",
|
|
68
|
-
description: null,
|
|
69
|
-
dateCreated: null,
|
|
70
|
-
...overrides,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
42
|
describe("toAmountMicros", () => {
|
|
75
43
|
it("converts $1,234.56 to 1234560000 micros", () => {
|
|
76
44
|
expect(toAmountMicros("$1,234.56")).toBe(1_234_560_000);
|
|
@@ -122,60 +90,27 @@ describe("normalizeEmail", () => {
|
|
|
122
90
|
});
|
|
123
91
|
});
|
|
124
92
|
|
|
125
|
-
describe("
|
|
126
|
-
it("maps
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
expect(mapLeadStatusToStage("20-Working").stage).toBe("LEAD_WORKING");
|
|
131
|
-
expect(mapLeadStatusToStage("30-Nurturing").stage).toBe("LEAD_WORKING");
|
|
132
|
-
expect(mapLeadStatusToStage("50-Qualified").stage).toBe("LEAD_QUALIFIED");
|
|
133
|
-
expect(mapLeadStatusToStage("90-Unqualified").stage).toBe(
|
|
134
|
-
"LEAD_UNQUALIFIED",
|
|
93
|
+
describe("mapDispatchCustomer", () => {
|
|
94
|
+
it("maps a cohort customer to a company keyed customer:<id>", () => {
|
|
95
|
+
const mapped = mapDispatchCustomer(
|
|
96
|
+
{ id: "cust_1", name: "Thunder Well Service", ownerRepId: "rep_jane" },
|
|
97
|
+
ownerMap,
|
|
135
98
|
);
|
|
136
|
-
expect(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
expect(mapLeadStatusToStage("13-Mystery")).toEqual({
|
|
141
|
-
stage: "LEAD",
|
|
142
|
-
unknown: true,
|
|
99
|
+
expect(mapped.input).toMatchObject({
|
|
100
|
+
name: "Thunder Well Service",
|
|
101
|
+
accountOwnerId: "member-jane-uuid",
|
|
102
|
+
sourceId: "customer:cust_1",
|
|
143
103
|
});
|
|
104
|
+
expect(mapped.warnings).toEqual([]);
|
|
144
105
|
});
|
|
145
106
|
|
|
146
|
-
it("
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
).
|
|
153
|
-
expect(
|
|
154
|
-
mapOpportunityStage({
|
|
155
|
-
stage: "40-Negotiate to Close",
|
|
156
|
-
closed: null,
|
|
157
|
-
won: null,
|
|
158
|
-
}).stage,
|
|
159
|
-
).toBe("NEGOTIATE");
|
|
160
|
-
expect(
|
|
161
|
-
mapOpportunityStage({
|
|
162
|
-
stage: "40-Negotiation to Close",
|
|
163
|
-
closed: null,
|
|
164
|
-
won: null,
|
|
165
|
-
}).stage,
|
|
166
|
-
).toBe("NEGOTIATE");
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("falls back to closed/won flags for blank stages", () => {
|
|
170
|
-
expect(
|
|
171
|
-
mapOpportunityStage({ stage: null, closed: "true", won: "true" }).stage,
|
|
172
|
-
).toBe("WON");
|
|
173
|
-
expect(
|
|
174
|
-
mapOpportunityStage({ stage: null, closed: "true", won: "false" }).stage,
|
|
175
|
-
).toBe("LOST");
|
|
176
|
-
expect(
|
|
177
|
-
mapOpportunityStage({ stage: null, closed: null, won: null }).stage,
|
|
178
|
-
).toBe("NEW");
|
|
107
|
+
it("warns on an unprovisioned owner and keeps the company", () => {
|
|
108
|
+
const mapped = mapDispatchCustomer(
|
|
109
|
+
{ id: "cust_2", name: "Beta Fuels", ownerRepId: "rep_gone" },
|
|
110
|
+
ownerMap,
|
|
111
|
+
);
|
|
112
|
+
expect(mapped.input).not.toHaveProperty("accountOwnerId");
|
|
113
|
+
expect(mapped.warnings[0]).toMatch(/rep_gone/);
|
|
179
114
|
});
|
|
180
115
|
});
|
|
181
116
|
|
|
@@ -247,59 +182,6 @@ describe("mapContact", () => {
|
|
|
247
182
|
});
|
|
248
183
|
});
|
|
249
184
|
|
|
250
|
-
describe("mapOpportunity", () => {
|
|
251
|
-
it("maps stage, micros, custom fields, links, and owner (AE1 shape)", () => {
|
|
252
|
-
const mapped = mapOpportunity(opportunity(), ownerMap, companyMap);
|
|
253
|
-
expect(mapped.input).toMatchObject({
|
|
254
|
-
name: "Bulk diesel contract",
|
|
255
|
-
stage: "PROSPECT",
|
|
256
|
-
amount: { amountMicros: 1_234_560_000, currencyCode: "USD" },
|
|
257
|
-
ownerId: "member-jane-uuid",
|
|
258
|
-
companyId: "company-uuid-1",
|
|
259
|
-
product: "Diesel",
|
|
260
|
-
quantity: 100,
|
|
261
|
-
isMobil: true,
|
|
262
|
-
sourceId: "opportunity:opp_1",
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it("is not Mobil when neither product nor brand mentions Mobil", () => {
|
|
267
|
-
const mapped = mapOpportunity(
|
|
268
|
-
opportunity({ brand: "Shell", productType: "Unleaded" }),
|
|
269
|
-
ownerMap,
|
|
270
|
-
companyMap,
|
|
271
|
-
);
|
|
272
|
-
expect(mapped.input.isMobil).toBe(false);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it("flags an unresolvable company and unprovisioned owner", () => {
|
|
276
|
-
const mapped = mapOpportunity(
|
|
277
|
-
opportunity({ accountId: "acct_missing", ownerRepId: "rep_gone" }),
|
|
278
|
-
ownerMap,
|
|
279
|
-
companyMap,
|
|
280
|
-
);
|
|
281
|
-
expect(mapped.input).not.toHaveProperty("companyId");
|
|
282
|
-
expect(mapped.input).not.toHaveProperty("ownerId");
|
|
283
|
-
expect(mapped.warnings).toHaveLength(2);
|
|
284
|
-
});
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
describe("mapLead", () => {
|
|
288
|
-
it("maps a lead row to the early Lead stage in the opportunity pipeline (AE3)", () => {
|
|
289
|
-
const mapped = mapLead(lead(), ownerMap);
|
|
290
|
-
expect(mapped.input).toMatchObject({
|
|
291
|
-
name: "Acme Fuel",
|
|
292
|
-
stage: "LEAD",
|
|
293
|
-
sourceId: "lead:lead_1",
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it("names person-only leads by their person name", () => {
|
|
298
|
-
const mapped = mapLead(lead({ companyName: null }), ownerMap);
|
|
299
|
-
expect(mapped.input.name).toBe("Ann Lee");
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
|
|
303
185
|
describe("notes mapping", () => {
|
|
304
186
|
it("maps a CRM comment onto its lead/opportunity target", () => {
|
|
305
187
|
const mapped = mapCrmComment({
|
|
@@ -309,6 +191,7 @@ describe("notes mapping", () => {
|
|
|
309
191
|
content: "Called the buyer.\nFollow up Friday.",
|
|
310
192
|
isDeleted: false,
|
|
311
193
|
createdAt: null,
|
|
194
|
+
authorName: null,
|
|
312
195
|
});
|
|
313
196
|
expect(mapped).toMatchObject({
|
|
314
197
|
sourceId: "task_comment:tc_1",
|
|
@@ -318,6 +201,22 @@ describe("notes mapping", () => {
|
|
|
318
201
|
});
|
|
319
202
|
});
|
|
320
203
|
|
|
204
|
+
it("carries the LastMile authored-at time and author onto the note", () => {
|
|
205
|
+
const mapped = mapCrmComment({
|
|
206
|
+
id: "tc_2",
|
|
207
|
+
entityType: "opportunity",
|
|
208
|
+
entityId: "opp_1",
|
|
209
|
+
content: "Sold them some products on cash account.",
|
|
210
|
+
isDeleted: false,
|
|
211
|
+
createdAt: new Date("2026-07-10T02:27:10.507Z"),
|
|
212
|
+
authorName: "Reyes Valdez",
|
|
213
|
+
});
|
|
214
|
+
// Twenty's activity feed sorts on createdAt; without this the whole history
|
|
215
|
+
// collapses onto the day the import ran.
|
|
216
|
+
expect(mapped.createdAt).toBe("2026-07-10T02:27:10.507Z");
|
|
217
|
+
expect(mapped.authorName).toBe("Reyes Valdez");
|
|
218
|
+
});
|
|
219
|
+
|
|
321
220
|
it("maps a customer note only when the customer uniquely matches an account", () => {
|
|
322
221
|
const base: LastmileCustomerNote = {
|
|
323
222
|
id: "n_1",
|
|
@@ -330,6 +229,72 @@ describe("notes mapping", () => {
|
|
|
330
229
|
expect(mapCustomerNote(base)?.targetSourceId).toBe("account:acct_1");
|
|
331
230
|
expect(mapCustomerNote({ ...base, matchedAccountId: null })).toBeNull();
|
|
332
231
|
});
|
|
232
|
+
|
|
233
|
+
it("reconstructs a status change as a dated note on its opportunity", () => {
|
|
234
|
+
const mapped = mapTaskStatusActivity({
|
|
235
|
+
id: "act_1",
|
|
236
|
+
entityType: "opportunity",
|
|
237
|
+
entityId: "opp_1",
|
|
238
|
+
oldStatusName: "00-New",
|
|
239
|
+
newStatusName: "10-Prospect",
|
|
240
|
+
createdAt: new Date("2026-05-14T20:19:03.740Z"),
|
|
241
|
+
authorName: "Dean Kittel",
|
|
242
|
+
});
|
|
243
|
+
expect(mapped).toMatchObject({
|
|
244
|
+
sourceId: "task_activity:act_1",
|
|
245
|
+
title: "Stage → 10-Prospect",
|
|
246
|
+
bodyMarkdown: "00-New → 10-Prospect",
|
|
247
|
+
targetSourceId: "opportunity:opp_1",
|
|
248
|
+
targetKind: "opportunity",
|
|
249
|
+
// True transition time, so the feed sorts correctly against the comments.
|
|
250
|
+
createdAt: "2026-05-14T20:19:03.740Z",
|
|
251
|
+
authorName: "Dean Kittel",
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("labels a status change with no prior stage as a plain set", () => {
|
|
256
|
+
const mapped = mapTaskStatusActivity({
|
|
257
|
+
id: "act_2",
|
|
258
|
+
entityType: "lead",
|
|
259
|
+
entityId: "lead_1",
|
|
260
|
+
oldStatusName: null,
|
|
261
|
+
newStatusName: "60-Won",
|
|
262
|
+
createdAt: null,
|
|
263
|
+
authorName: null,
|
|
264
|
+
});
|
|
265
|
+
expect(mapped?.bodyMarkdown).toBe("Set to 60-Won");
|
|
266
|
+
expect(mapped?.targetSourceId).toBe("lead:lead_1");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("drops a status event that names no destination stage", () => {
|
|
270
|
+
expect(
|
|
271
|
+
mapTaskStatusActivity({
|
|
272
|
+
id: "act_3",
|
|
273
|
+
entityType: "opportunity",
|
|
274
|
+
entityId: "opp_1",
|
|
275
|
+
oldStatusName: "10-Prospect",
|
|
276
|
+
newStatusName: null,
|
|
277
|
+
createdAt: null,
|
|
278
|
+
authorName: null,
|
|
279
|
+
}),
|
|
280
|
+
).toBeNull();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe("toIsoTimestamp", () => {
|
|
285
|
+
it("converts a Date to ISO-8601 and passes through parseable strings", () => {
|
|
286
|
+
expect(toIsoTimestamp(new Date("2025-07-21T12:00:00Z"))).toBe(
|
|
287
|
+
"2025-07-21T12:00:00.000Z",
|
|
288
|
+
);
|
|
289
|
+
expect(toIsoTimestamp("2026-07-10T02:27:10.507Z")).toBe(
|
|
290
|
+
"2026-07-10T02:27:10.507Z",
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("returns null for missing or unparseable values", () => {
|
|
295
|
+
expect(toIsoTimestamp(null)).toBeNull();
|
|
296
|
+
expect(toIsoTimestamp("not a date")).toBeNull();
|
|
297
|
+
});
|
|
333
298
|
});
|
|
334
299
|
|
|
335
300
|
describe("contentHash", () => {
|
|
@@ -443,13 +408,13 @@ describe("mapOpportunityProduct (multiple products per opportunity)", () => {
|
|
|
443
408
|
expect(mapped.sourceId).toBe("opportunity_item:opp_1#0");
|
|
444
409
|
expect(mapped.opportunitySourceId).toBe("opportunity:opp_1");
|
|
445
410
|
expect(mapped.input).toMatchObject({
|
|
446
|
-
name: "
|
|
447
|
-
product: "MOBIL",
|
|
411
|
+
name: "Mobil",
|
|
448
412
|
quantity: 6500,
|
|
449
413
|
amount: { amountMicros: 50_000_000_000, currencyCode: "USD" },
|
|
450
414
|
isMobil: true,
|
|
451
415
|
lineNumber: 1,
|
|
452
416
|
});
|
|
417
|
+
expect(mapped.productSourceId).toBe("product:mobil");
|
|
453
418
|
});
|
|
454
419
|
|
|
455
420
|
it("gives each line on one opportunity a distinct, stable sourceId", () => {
|
|
@@ -465,6 +430,11 @@ describe("mapOpportunityProduct (multiple products per opportunity)", () => {
|
|
|
465
430
|
]);
|
|
466
431
|
expect(new Set(lines.map((l) => l.opportunitySourceId)).size).toBe(1);
|
|
467
432
|
expect(lines.map((l) => l.input.isMobil)).toEqual([false, false, true]);
|
|
433
|
+
expect(lines.map((l) => l.productSourceId)).toEqual([
|
|
434
|
+
"product:def",
|
|
435
|
+
"product:golden_west",
|
|
436
|
+
"product:mobil",
|
|
437
|
+
]);
|
|
468
438
|
expect(lines.map((l) => l.input.lineNumber)).toEqual([1, 2, 3]);
|
|
469
439
|
});
|
|
470
440
|
|
|
@@ -473,7 +443,7 @@ describe("mapOpportunityProduct (multiple products per opportunity)", () => {
|
|
|
473
443
|
item({ brand: null, quantity: "n/a", amount: "" }),
|
|
474
444
|
);
|
|
475
445
|
expect(mapped.input.name).toBe("Line 1");
|
|
476
|
-
expect(mapped.
|
|
446
|
+
expect(mapped.productSourceId).toBeNull();
|
|
477
447
|
expect(mapped.input).not.toHaveProperty("quantity");
|
|
478
448
|
expect(mapped.input).not.toHaveProperty("amount");
|
|
479
449
|
expect(mapped.input.isMobil).toBe(false);
|
|
@@ -487,3 +457,279 @@ describe("mapOpportunityProduct (multiple products per opportunity)", () => {
|
|
|
487
457
|
expect(isMobilBrand(null)).toBe(false);
|
|
488
458
|
});
|
|
489
459
|
});
|
|
460
|
+
|
|
461
|
+
describe("mapTaskStatusToStage", () => {
|
|
462
|
+
it("maps LastMile status names verbatim to pipeline stages", () => {
|
|
463
|
+
expect(mapTaskStatusToStage("10-Prospect").stage).toBe("PROSPECT");
|
|
464
|
+
expect(mapTaskStatusToStage("60-Won").stage).toBe("WON");
|
|
465
|
+
expect(mapTaskStatusToStage("20-Account Needs").stage).toBe(
|
|
466
|
+
"ACCOUNT_NEEDS",
|
|
467
|
+
);
|
|
468
|
+
expect(mapTaskStatusToStage("Converted").stage).toBe("CONVERTED");
|
|
469
|
+
expect(mapTaskStatusToStage("90-Unqualified").stage).toBe("UNQUALIFIED");
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("flags an unmapped status instead of silently bucketing it", () => {
|
|
473
|
+
expect(mapTaskStatusToStage("Zz-Unknown")).toEqual({
|
|
474
|
+
stage: "NEW",
|
|
475
|
+
unknown: true,
|
|
476
|
+
});
|
|
477
|
+
expect(mapTaskStatusToStage(null).unknown).toBe(true);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
describe("mapCrmTask (task table is the CRM authority)", () => {
|
|
482
|
+
const ownerIndex = new Map([["rep_chad", "member-chad"]]);
|
|
483
|
+
const companyMap = new Map([["account:acct_reign", "company-reign"]]);
|
|
484
|
+
const orgMap = new Map([["organization:org_gwo", "org-gwo-300"]]);
|
|
485
|
+
|
|
486
|
+
function task(overrides: Partial<LastmileCrmTask> = {}): LastmileCrmTask {
|
|
487
|
+
return {
|
|
488
|
+
taskId: "task_xdh6577weuhsc2ttlct1acyl",
|
|
489
|
+
entityType: "opportunity",
|
|
490
|
+
entityId: "opp_xdh6577weuhsc2ttlct1acyl",
|
|
491
|
+
title: "Reign Rentals GW Lubes",
|
|
492
|
+
description: "start up oilfield rental company with brand new equipment",
|
|
493
|
+
accountId: "acct_reign",
|
|
494
|
+
leadCompanyName: null,
|
|
495
|
+
statusName: "10-Prospect",
|
|
496
|
+
organizationId: "org_gwo",
|
|
497
|
+
assigneeRepId: "rep_chad",
|
|
498
|
+
dueDate: new Date("2026-07-23T00:00:00Z"),
|
|
499
|
+
createdAt: null,
|
|
500
|
+
items: [{ brand: "Golden West", amount: 12000, quantity: 40 }],
|
|
501
|
+
...overrides,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
it("reproduces the reference opportunity exactly", () => {
|
|
506
|
+
const mapped = mapCrmTask(task(), ownerIndex, companyMap, orgMap);
|
|
507
|
+
expect(mapped.sourceId).toBe("opportunity:opp_xdh6577weuhsc2ttlct1acyl");
|
|
508
|
+
expect(mapped.input).toMatchObject({
|
|
509
|
+
name: "Reign Rentals GW Lubes",
|
|
510
|
+
stage: "PROSPECT",
|
|
511
|
+
amount: { amountMicros: 12_000_000_000, currencyCode: "USD" },
|
|
512
|
+
ownerId: "member-chad",
|
|
513
|
+
companyId: "company-reign",
|
|
514
|
+
organizationId: "org-gwo-300",
|
|
515
|
+
isMobil: false,
|
|
516
|
+
});
|
|
517
|
+
expect(mapped.warnings).toEqual([]);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("replays the LastMile creation time onto Twenty's createdAt", () => {
|
|
521
|
+
const mapped = mapCrmTask(
|
|
522
|
+
task({ createdAt: new Date("2026-05-13T15:37:15.268Z") }),
|
|
523
|
+
ownerIndex,
|
|
524
|
+
companyMap,
|
|
525
|
+
orgMap,
|
|
526
|
+
);
|
|
527
|
+
expect(mapped.input.createdAt).toBe("2026-05-13T15:37:15.268Z");
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("omits createdAt when the task has no creation time", () => {
|
|
531
|
+
const mapped = mapCrmTask(task(), ownerIndex, companyMap, orgMap);
|
|
532
|
+
expect(mapped.input).not.toHaveProperty("createdAt");
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it("takes status from the task, not the stale opportunity.stage column", () => {
|
|
536
|
+
// The opportunity row for this task says "30-Formulate Offer"; the task
|
|
537
|
+
// says 10-Prospect, and the task wins.
|
|
538
|
+
expect(mapCrmTask(task(), ownerIndex, companyMap, orgMap).input.stage).toBe(
|
|
539
|
+
"PROSPECT",
|
|
540
|
+
);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("sums product lines into the deal amount and rolls up isMobil", () => {
|
|
544
|
+
const mapped = mapCrmTask(
|
|
545
|
+
task({
|
|
546
|
+
items: [
|
|
547
|
+
{ brand: "DEF", amount: 1000, quantity: 1000 },
|
|
548
|
+
{ brand: "GOLDEN WEST", amount: 1000, quantity: 1000 },
|
|
549
|
+
{ brand: "MOBIL", amount: 1000, quantity: 500 },
|
|
550
|
+
],
|
|
551
|
+
}),
|
|
552
|
+
ownerIndex,
|
|
553
|
+
companyMap,
|
|
554
|
+
orgMap,
|
|
555
|
+
);
|
|
556
|
+
expect(mapped.input.amount).toEqual({
|
|
557
|
+
amountMicros: 3_000_000_000,
|
|
558
|
+
currencyCode: "USD",
|
|
559
|
+
});
|
|
560
|
+
expect(mapped.input.isMobil).toBe(true);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it("maps a lead with no account to a named opportunity with no company", () => {
|
|
564
|
+
const mapped = mapCrmTask(
|
|
565
|
+
task({
|
|
566
|
+
entityType: "lead",
|
|
567
|
+
entityId: "lead_1",
|
|
568
|
+
title: null,
|
|
569
|
+
accountId: null,
|
|
570
|
+
leadCompanyName: "Alpine Silica",
|
|
571
|
+
statusName: "50-Qualified",
|
|
572
|
+
items: null,
|
|
573
|
+
}),
|
|
574
|
+
ownerIndex,
|
|
575
|
+
companyMap,
|
|
576
|
+
orgMap,
|
|
577
|
+
);
|
|
578
|
+
expect(mapped.sourceId).toBe("lead:lead_1");
|
|
579
|
+
expect(mapped.input).toMatchObject({
|
|
580
|
+
name: "Alpine Silica",
|
|
581
|
+
stage: "QUALIFIED",
|
|
582
|
+
});
|
|
583
|
+
expect(mapped.input).not.toHaveProperty("companyId");
|
|
584
|
+
expect(mapped.input).not.toHaveProperty("amount");
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it("flags an unassigned task and an unmigrated organization", () => {
|
|
588
|
+
const mapped = mapCrmTask(
|
|
589
|
+
task({ assigneeRepId: null, organizationId: "org_missing" }),
|
|
590
|
+
ownerIndex,
|
|
591
|
+
companyMap,
|
|
592
|
+
orgMap,
|
|
593
|
+
);
|
|
594
|
+
expect(mapped.input).not.toHaveProperty("ownerId");
|
|
595
|
+
expect(mapped.input).not.toHaveProperty("organizationId");
|
|
596
|
+
expect(mapped.warnings).toEqual([
|
|
597
|
+
"task has no assignee; no owner",
|
|
598
|
+
"organization org_missing not migrated",
|
|
599
|
+
]);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it("derives product lines from the task's items array", () => {
|
|
603
|
+
const lines = mapTaskProducts(task());
|
|
604
|
+
expect(lines).toHaveLength(1);
|
|
605
|
+
expect(lines[0]).toMatchObject({
|
|
606
|
+
sourceId: "opportunity_item:opp_xdh6577weuhsc2ttlct1acyl#0",
|
|
607
|
+
opportunitySourceId: "opportunity:opp_xdh6577weuhsc2ttlct1acyl",
|
|
608
|
+
});
|
|
609
|
+
expect(lines[0].input).toMatchObject({
|
|
610
|
+
name: "Golden West",
|
|
611
|
+
quantity: 40,
|
|
612
|
+
amount: { amountMicros: 12_000_000_000, currencyCode: "USD" },
|
|
613
|
+
});
|
|
614
|
+
expect(lines[0].productSourceId).toBe("product:golden_west");
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
describe("mapOrganization", () => {
|
|
619
|
+
it("names the record by its abbreviation and keeps the full name", () => {
|
|
620
|
+
const mapped = mapOrganization({
|
|
621
|
+
id: "org_x3wdgjtw4x4jqx2a937amutm",
|
|
622
|
+
name: "Golden West Oil Co..San Antonio (300)",
|
|
623
|
+
abbv: "GWO 300",
|
|
624
|
+
archived: false,
|
|
625
|
+
});
|
|
626
|
+
expect(mapped.sourceId).toBe("organization:org_x3wdgjtw4x4jqx2a937amutm");
|
|
627
|
+
expect(mapped.input).toMatchObject({
|
|
628
|
+
name: "GWO 300",
|
|
629
|
+
fullName: "Golden West Oil Co..San Antonio (300)",
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it("falls back to the full name when there is no abbreviation", () => {
|
|
634
|
+
const mapped = mapOrganization({
|
|
635
|
+
id: "org_1",
|
|
636
|
+
name: "UNKNOWN",
|
|
637
|
+
abbv: null,
|
|
638
|
+
archived: false,
|
|
639
|
+
});
|
|
640
|
+
expect(mapped.input.name).toBe("UNKNOWN");
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
describe("dedupeContactEmails", () => {
|
|
645
|
+
it("gives a shared address to the first contact by id and clears the rest", () => {
|
|
646
|
+
const result = dedupeContactEmails([
|
|
647
|
+
{ id: "cont_b", email: "test@test.com" },
|
|
648
|
+
{ id: "cont_a", email: "Test@Test.com" },
|
|
649
|
+
{ id: "cont_c", email: "unique@tei.com" },
|
|
650
|
+
]);
|
|
651
|
+
const byId = new Map(result.map((c) => [c.id, c.email]));
|
|
652
|
+
expect(byId.get("cont_a")).toBe("Test@Test.com"); // first by id keeps it
|
|
653
|
+
expect(byId.get("cont_b")).toBeNull();
|
|
654
|
+
expect(byId.get("cont_c")).toBe("unique@tei.com");
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it("is deterministic regardless of input order", () => {
|
|
658
|
+
const rows = [
|
|
659
|
+
{ id: "cont_a", email: "x@y.com" },
|
|
660
|
+
{ id: "cont_b", email: "x@y.com" },
|
|
661
|
+
];
|
|
662
|
+
const forward = dedupeContactEmails(rows);
|
|
663
|
+
const reversed = dedupeContactEmails([...rows].reverse());
|
|
664
|
+
expect(forward.map((c) => [c.id, c.email])).toEqual(
|
|
665
|
+
reversed.map((c) => [c.id, c.email]),
|
|
666
|
+
);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it("leaves invalid and missing emails alone", () => {
|
|
670
|
+
const result = dedupeContactEmails([
|
|
671
|
+
{ id: "a", email: null },
|
|
672
|
+
{ id: "b", email: "garbage" },
|
|
673
|
+
{ id: "c", email: "garbage" },
|
|
674
|
+
]);
|
|
675
|
+
expect(result.map((c) => c.email)).toEqual([null, "garbage", "garbage"]);
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
describe("normalizeProductName (19 LastMile spellings -> 7 catalog products)", () => {
|
|
680
|
+
it("folds channel suffixes into the parent product", () => {
|
|
681
|
+
// CVL / PVL / INDUSTRIAL denote the sales channel, not a different product.
|
|
682
|
+
expect(normalizeProductName("MOBIL - CVL")).toBe("Mobil");
|
|
683
|
+
expect(normalizeProductName("MOBIL - PVL")).toBe("Mobil");
|
|
684
|
+
expect(normalizeProductName("MOBIL - INDUSTRIAL")).toBe("Mobil");
|
|
685
|
+
expect(normalizeProductName("GWO - CVL")).toBe("Golden West");
|
|
686
|
+
expect(normalizeProductName("GWO - PVL")).toBe("Golden West");
|
|
687
|
+
expect(normalizeProductName("GWO - INDUSTRIAL")).toBe("Golden West");
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it("is case-insensitive across every observed spelling", () => {
|
|
691
|
+
expect(normalizeProductName("MOBIL")).toBe("Mobil");
|
|
692
|
+
expect(normalizeProductName("Mobil")).toBe("Mobil");
|
|
693
|
+
expect(normalizeProductName("GOLDEN WEST")).toBe("Golden West");
|
|
694
|
+
expect(normalizeProductName("Golden West")).toBe("Golden West");
|
|
695
|
+
expect(normalizeProductName("FUEL")).toBe("Fuel");
|
|
696
|
+
expect(normalizeProductName("DEF")).toBe("DEF");
|
|
697
|
+
expect(normalizeProductName("MIGHTY")).toBe("Mighty");
|
|
698
|
+
expect(normalizeProductName("Mighty")).toBe("Mighty");
|
|
699
|
+
expect(normalizeProductName("ANCILLARY")).toBe("Ancillary");
|
|
700
|
+
expect(normalizeProductName("HOTSY")).toBe("Hotsy");
|
|
701
|
+
expect(normalizeProductName("Hotsy")).toBe("Hotsy");
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it("every catalog name maps to itself", () => {
|
|
705
|
+
for (const name of PRODUCT_CATALOG) {
|
|
706
|
+
expect(normalizeProductName(name)).toBe(name);
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it("refuses to guess: UNKNOWN and blanks map to nothing", () => {
|
|
711
|
+
expect(normalizeProductName("UNKNOWN")).toBeNull();
|
|
712
|
+
expect(normalizeProductName("")).toBeNull();
|
|
713
|
+
expect(normalizeProductName(" ")).toBeNull();
|
|
714
|
+
expect(normalizeProductName(null)).toBeNull();
|
|
715
|
+
expect(normalizeProductName("Kerosene")).toBeNull();
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it("isMobil follows the catalog, not the raw string", () => {
|
|
719
|
+
expect(isMobilBrand("MOBIL - CVL")).toBe(true);
|
|
720
|
+
expect(isMobilBrand("Mobil")).toBe(true);
|
|
721
|
+
expect(isMobilBrand("GOLDEN WEST")).toBe(false);
|
|
722
|
+
expect(isMobilBrand("UNKNOWN")).toBe(false);
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
describe("mapProduct", () => {
|
|
727
|
+
it("gives each catalog product a stable sourceId", () => {
|
|
728
|
+
expect(mapProduct("Golden West")).toMatchObject({
|
|
729
|
+
sourceId: "product:golden_west",
|
|
730
|
+
input: { name: "Golden West", sourceId: "product:golden_west" },
|
|
731
|
+
});
|
|
732
|
+
expect(mapProduct("DEF").sourceId).toBe("product:def");
|
|
733
|
+
expect(productSourceId("Mobil")).toBe("product:mobil");
|
|
734
|
+
});
|
|
735
|
+
});
|