integration-automation-patterns 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ashutosh Rana
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: integration-automation-patterns
3
+ Version: 0.1.0
4
+ Summary: Reference patterns for reliable enterprise integration, workflow automation, and system-of-record synchronization.
5
+ Author: Ashutosh Rana
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ashutoshrana/integration-automation-patterns
8
+ Project-URL: Issues, https://github.com/ashutoshrana/integration-automation-patterns/issues
9
+ Project-URL: Changelog, https://github.com/ashutoshrana/integration-automation-patterns/blob/main/CHANGELOG.md
10
+ Keywords: enterprise-integration,event-driven,workflow-automation,crm,erp,idempotency,system-of-record,integration-patterns,enterprise-architecture
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: System :: Distributed Computing
20
+ Classifier: Topic :: Office/Business
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: test
25
+ Requires-Dist: pytest>=7.0; extra == "test"
26
+ Requires-Dist: pytest-cov>=4.0; extra == "test"
27
+ Provides-Extra: lint
28
+ Requires-Dist: ruff>=0.4.0; extra == "lint"
29
+ Provides-Extra: typecheck
30
+ Requires-Dist: mypy>=1.0; extra == "typecheck"
31
+ Provides-Extra: dev
32
+ Requires-Dist: integration-automation-patterns[lint,test,typecheck]; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # integration-automation-patterns
36
+
37
+ [![CI](https://github.com/ashutoshrana/integration-automation-patterns/actions/workflows/ci.yml/badge.svg)](https://github.com/ashutoshrana/integration-automation-patterns/actions/workflows/ci.yml)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
39
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
40
+ [![PyPI](https://img.shields.io/pypi/v/integration-automation-patterns.svg)](https://pypi.org/project/integration-automation-patterns/)
41
+
42
+ Practical patterns for enterprise integration, workflow orchestration, and system-of-record synchronization in complex operating environments.
43
+
44
+ ## Why this repo exists
45
+
46
+ Enterprise modernization usually breaks down at the integration layer:
47
+ - brittle handoffs between systems
48
+ - inconsistent event handling
49
+ - weak retry and idempotency models
50
+ - workflow logic scattered across tools
51
+ - poor visibility into operational state
52
+
53
+ This repository is a public-safe reference for patterns that help teams build more reliable integration and automation systems. The patterns are platform-agnostic and cloud-agnostic — applicable across any combination of CRM, ERP, ITSM, and custom services, on any cloud environment (AWS, GCP, Azure, OCI) or on-premises.
54
+
55
+ ## Scope
56
+
57
+ This repo focuses on:
58
+ - event-driven integration patterns with explicit retry and idempotency models
59
+ - system-of-record synchronization with authority boundaries
60
+ - workflow orchestration and escalation boundaries
61
+ - observability for automation flows
62
+ - public-safe architecture notes for enterprise operations
63
+
64
+ The patterns do not assume any specific vendor, broker, or cloud platform.
65
+
66
+ ## Modules
67
+
68
+ - `event_envelope.py`
69
+ Reliable event transport with explicit delivery status, bounded retry policy,
70
+ and structured audit logging. Works with any message broker (Kafka, SQS,
71
+ Azure Service Bus, GCP Pub/Sub, RabbitMQ, IBM MQ, and others).
72
+
73
+ - `sync_boundary.py`
74
+ System-of-record synchronization contracts for bi-directional integration
75
+ between enterprise platforms. Explicit field-level authority assignment,
76
+ conflict detection, and exclusion management. Platform-agnostic.
77
+
78
+ ## Repository structure
79
+
80
+ - `src/integration_automation_patterns/`
81
+ - `event_envelope.py` — event transport with retry and audit
82
+ - `sync_boundary.py` — bi-directional sync authority boundaries
83
+ - `docs/architecture.md`
84
+ - `docs/implementation-note-01.md`
85
+ - `docs/adr/`
86
+ - `examples/event-flow.yaml`
87
+ - `CITATION.cff`
88
+ - `CONTRIBUTING.md`
89
+ - `GOVERNANCE.md`
90
+
91
+ ## Near-term roadmap
92
+
93
+ - add integration reliability ADRs
94
+ - add examples for retry-safe event handling across broker types
95
+ - document action logging and audit boundaries
96
+ - add workflow orchestration boundary patterns
97
+
98
+ ## Published notes
99
+
100
+ - implementation note: [`docs/implementation-note-01.md`](./docs/implementation-note-01.md)
101
+
102
+ ## Intended audience
103
+
104
+ - enterprise architects
105
+ - integration engineers
106
+ - workflow and automation operators
107
+ - platform teams responsible for system-of-record reliability across CRM, ERP, and service platforms
108
+
109
+ ## Citing this work
110
+
111
+ If you use these patterns in your work, see `CITATION.cff` or use GitHub's "Cite this repository" button above.
@@ -0,0 +1,77 @@
1
+ # integration-automation-patterns
2
+
3
+ [![CI](https://github.com/ashutoshrana/integration-automation-patterns/actions/workflows/ci.yml/badge.svg)](https://github.com/ashutoshrana/integration-automation-patterns/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
6
+ [![PyPI](https://img.shields.io/pypi/v/integration-automation-patterns.svg)](https://pypi.org/project/integration-automation-patterns/)
7
+
8
+ Practical patterns for enterprise integration, workflow orchestration, and system-of-record synchronization in complex operating environments.
9
+
10
+ ## Why this repo exists
11
+
12
+ Enterprise modernization usually breaks down at the integration layer:
13
+ - brittle handoffs between systems
14
+ - inconsistent event handling
15
+ - weak retry and idempotency models
16
+ - workflow logic scattered across tools
17
+ - poor visibility into operational state
18
+
19
+ This repository is a public-safe reference for patterns that help teams build more reliable integration and automation systems. The patterns are platform-agnostic and cloud-agnostic — applicable across any combination of CRM, ERP, ITSM, and custom services, on any cloud environment (AWS, GCP, Azure, OCI) or on-premises.
20
+
21
+ ## Scope
22
+
23
+ This repo focuses on:
24
+ - event-driven integration patterns with explicit retry and idempotency models
25
+ - system-of-record synchronization with authority boundaries
26
+ - workflow orchestration and escalation boundaries
27
+ - observability for automation flows
28
+ - public-safe architecture notes for enterprise operations
29
+
30
+ The patterns do not assume any specific vendor, broker, or cloud platform.
31
+
32
+ ## Modules
33
+
34
+ - `event_envelope.py`
35
+ Reliable event transport with explicit delivery status, bounded retry policy,
36
+ and structured audit logging. Works with any message broker (Kafka, SQS,
37
+ Azure Service Bus, GCP Pub/Sub, RabbitMQ, IBM MQ, and others).
38
+
39
+ - `sync_boundary.py`
40
+ System-of-record synchronization contracts for bi-directional integration
41
+ between enterprise platforms. Explicit field-level authority assignment,
42
+ conflict detection, and exclusion management. Platform-agnostic.
43
+
44
+ ## Repository structure
45
+
46
+ - `src/integration_automation_patterns/`
47
+ - `event_envelope.py` — event transport with retry and audit
48
+ - `sync_boundary.py` — bi-directional sync authority boundaries
49
+ - `docs/architecture.md`
50
+ - `docs/implementation-note-01.md`
51
+ - `docs/adr/`
52
+ - `examples/event-flow.yaml`
53
+ - `CITATION.cff`
54
+ - `CONTRIBUTING.md`
55
+ - `GOVERNANCE.md`
56
+
57
+ ## Near-term roadmap
58
+
59
+ - add integration reliability ADRs
60
+ - add examples for retry-safe event handling across broker types
61
+ - document action logging and audit boundaries
62
+ - add workflow orchestration boundary patterns
63
+
64
+ ## Published notes
65
+
66
+ - implementation note: [`docs/implementation-note-01.md`](./docs/implementation-note-01.md)
67
+
68
+ ## Intended audience
69
+
70
+ - enterprise architects
71
+ - integration engineers
72
+ - workflow and automation operators
73
+ - platform teams responsible for system-of-record reliability across CRM, ERP, and service platforms
74
+
75
+ ## Citing this work
76
+
77
+ If you use these patterns in your work, see `CITATION.cff` or use GitHub's "Cite this repository" button above.
@@ -0,0 +1,73 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "integration-automation-patterns"
7
+ version = "0.1.0"
8
+ description = "Reference patterns for reliable enterprise integration, workflow automation, and system-of-record synchronization."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Ashutosh Rana" }
14
+ ]
15
+ keywords = [
16
+ "enterprise-integration",
17
+ "event-driven",
18
+ "workflow-automation",
19
+ "crm",
20
+ "erp",
21
+ "idempotency",
22
+ "system-of-record",
23
+ "integration-patterns",
24
+ "enterprise-architecture",
25
+ ]
26
+ classifiers = [
27
+ "Development Status :: 4 - Beta",
28
+ "Intended Audience :: Developers",
29
+ "License :: OSI Approved :: MIT License",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.10",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Topic :: Software Development :: Libraries :: Python Modules",
35
+ "Topic :: System :: Distributed Computing",
36
+ "Topic :: Office/Business",
37
+ ]
38
+ dependencies = []
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/ashutoshrana/integration-automation-patterns"
42
+ Issues = "https://github.com/ashutoshrana/integration-automation-patterns/issues"
43
+ Changelog = "https://github.com/ashutoshrana/integration-automation-patterns/blob/main/CHANGELOG.md"
44
+
45
+ [project.optional-dependencies]
46
+ test = ["pytest>=7.0", "pytest-cov>=4.0"]
47
+ lint = ["ruff>=0.4.0"]
48
+ typecheck = ["mypy>=1.0"]
49
+ dev = [
50
+ "integration-automation-patterns[test,lint,typecheck]",
51
+ ]
52
+
53
+ [tool.setuptools]
54
+ package-dir = {"" = "src"}
55
+
56
+ [tool.setuptools.packages.find]
57
+ where = ["src"]
58
+
59
+ [tool.pytest.ini_options]
60
+ testpaths = ["tests"]
61
+ addopts = "-v --tb=short"
62
+
63
+ [tool.ruff]
64
+ line-length = 100
65
+ target-version = "py310"
66
+
67
+ [tool.ruff.lint]
68
+ select = ["E", "F", "I", "UP"]
69
+
70
+ [tool.mypy]
71
+ python_version = "3.10"
72
+ strict = true
73
+ ignore_missing_imports = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,15 @@
1
+ """Reference patterns for reliable enterprise integration and workflow automation."""
2
+
3
+ from .event_envelope import DeliveryStatus, EventEnvelope, RetryPolicy
4
+ from .sync_boundary import RecordAuthority, SyncBoundary, SyncConflict
5
+
6
+ __all__ = [
7
+ # Event handling
8
+ "DeliveryStatus",
9
+ "EventEnvelope",
10
+ "RetryPolicy",
11
+ # Sync boundary
12
+ "RecordAuthority",
13
+ "SyncBoundary",
14
+ "SyncConflict",
15
+ ]
@@ -0,0 +1,206 @@
1
+ """
2
+ event_envelope.py — Reliable Event Handling for Enterprise Integration
3
+
4
+ Enterprise integration fails most often at the transport layer — not because
5
+ the business logic is wrong, but because events are dropped, duplicated, or
6
+ processed out of order when systems restart, network partitions occur, or
7
+ downstream services are temporarily unavailable.
8
+
9
+ This module provides the structural primitives for building reliable,
10
+ replay-safe event handling in enterprise integration workflows. The patterns
11
+ apply regardless of the message broker (Kafka, SQS, Azure Service Bus,
12
+ GCP Pub/Sub, RabbitMQ, MQ Series) or the enterprise platforms being integrated
13
+ (CRM, ERP, ITSM, custom services).
14
+
15
+ Design goals:
16
+ - Every event carries enough context to be replayed safely
17
+ - Retry behavior is explicit and bounded, not implicit
18
+ - Delivery status is inspectable without querying the broker
19
+ - Deduplication is structural (idempotency key), not a side effect of storage
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from dataclasses import dataclass, field
25
+ from datetime import datetime, timezone
26
+ from enum import Enum
27
+ from typing import Any
28
+
29
+
30
+ class DeliveryStatus(Enum):
31
+ """
32
+ Tracks the lifecycle of one event through the integration pipeline.
33
+
34
+ Events move forward through this lifecycle. An event that has reached
35
+ FAILED should not be silently dropped — it should be routed to a dead
36
+ letter queue or an explicit remediation path.
37
+ """
38
+ PENDING = "pending" # Created, not yet dispatched
39
+ DISPATCHED = "dispatched" # Sent to broker or downstream system
40
+ ACKNOWLEDGED = "acknowledged" # Downstream confirmed receipt
41
+ PROCESSED = "processed" # Business logic completed successfully
42
+ RETRYING = "retrying" # Failed at least once, within retry budget
43
+ FAILED = "failed" # Retry budget exhausted; requires intervention
44
+ SKIPPED = "skipped" # Intentionally not processed (e.g., duplicate)
45
+
46
+
47
+ @dataclass(slots=True)
48
+ class RetryPolicy:
49
+ """
50
+ Defines retry behavior for a failed event delivery attempt.
51
+
52
+ Explicit retry policies prevent two common failure modes:
53
+ 1. Infinite retry loops that mask persistent downstream failures
54
+ 2. Retry storms that overwhelm recovering services
55
+
56
+ Attributes:
57
+ max_attempts: Total number of delivery attempts allowed (including
58
+ the first). After this many attempts, the event moves to FAILED.
59
+ backoff_seconds: Base wait time between retries in seconds.
60
+ Actual wait = backoff_seconds * (2 ** attempt_number) when
61
+ exponential = True.
62
+ exponential: If True, use exponential backoff. If False, use
63
+ fixed-interval retry.
64
+ max_backoff_seconds: Upper bound on wait time when using exponential
65
+ backoff. Prevents unbounded delays.
66
+ """
67
+ max_attempts: int = 3
68
+ backoff_seconds: int = 30
69
+ exponential: bool = True
70
+ max_backoff_seconds: int = 3600
71
+
72
+ def wait_seconds_for_attempt(self, attempt_number: int) -> int:
73
+ """
74
+ Calculate the wait time before the given attempt number.
75
+
76
+ Args:
77
+ attempt_number: Zero-indexed attempt count (0 = first retry,
78
+ not the original attempt).
79
+
80
+ Returns:
81
+ Seconds to wait before attempting delivery again.
82
+ """
83
+ if not self.exponential:
84
+ return self.backoff_seconds
85
+ delay = self.backoff_seconds * (2 ** attempt_number)
86
+ return min(delay, self.max_backoff_seconds)
87
+
88
+ def is_exhausted(self, attempt_number: int) -> bool:
89
+ """Return True if the attempt budget is exhausted."""
90
+ return attempt_number >= self.max_attempts
91
+
92
+
93
+ @dataclass
94
+ class EventEnvelope:
95
+ """
96
+ A transport container for one unit of work in an enterprise integration flow.
97
+
98
+ An EventEnvelope carries the event payload alongside the metadata needed
99
+ to route, deduplicate, retry, and audit it. Separating envelope metadata
100
+ from business payload means integration infrastructure can handle
101
+ reliability concerns without touching business logic.
102
+
103
+ Design principle: The envelope must contain everything needed to replay
104
+ the event safely, without requiring coordination with the original sender.
105
+
106
+ Attributes:
107
+ event_id: Globally unique identifier for this event. Used as the
108
+ idempotency key — processing the same event_id twice should
109
+ produce the same result as processing it once.
110
+ event_type: A namespaced string identifying what happened.
111
+ Convention: "<domain>.<entity>.<action>" (e.g., "enrollment.student.updated")
112
+ source_system: Identifier for the system that originated the event.
113
+ Used for routing, filtering, and audit.
114
+ payload: The business data associated with the event. Keep payloads
115
+ small — prefer IDs + change summary over full record snapshots.
116
+ Full records should be fetched from the system of record at
117
+ processing time, not embedded in the event.
118
+ correlation_id: Links this event to a parent workflow, request, or
119
+ transaction. Enables tracing across system boundaries.
120
+ schema_version: Semantic version of the payload schema. Consumers
121
+ should reject events with unexpected versions rather than
122
+ silently misinterpreting the payload.
123
+ created_at: Wall clock time when the event was created. Stored in
124
+ UTC. Do not use for business ordering — use a sequence or
125
+ logical clock for that.
126
+ status: Current delivery status. Updated as the event moves through
127
+ the pipeline.
128
+ attempt_count: Number of delivery attempts made. Starts at 0,
129
+ incremented before each attempt.
130
+ retry_policy: Retry behavior for failed delivery attempts.
131
+ tags: Arbitrary key-value metadata for routing, filtering, or
132
+ observability. Not used in business logic.
133
+ """
134
+ event_id: str
135
+ event_type: str
136
+ source_system: str
137
+ payload: dict[str, Any]
138
+ correlation_id: str = ""
139
+ schema_version: str = "1.0"
140
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
141
+ status: DeliveryStatus = DeliveryStatus.PENDING
142
+ attempt_count: int = 0
143
+ retry_policy: RetryPolicy = field(default_factory=RetryPolicy)
144
+ tags: dict[str, str] = field(default_factory=dict)
145
+
146
+ def mark_dispatched(self) -> None:
147
+ """Record that this event has been sent to the broker or downstream system."""
148
+ self.attempt_count += 1
149
+ self.status = DeliveryStatus.DISPATCHED
150
+
151
+ def mark_acknowledged(self) -> None:
152
+ """Record that the downstream system confirmed receipt."""
153
+ self.status = DeliveryStatus.ACKNOWLEDGED
154
+
155
+ def mark_processed(self) -> None:
156
+ """Record that business processing completed successfully."""
157
+ self.status = DeliveryStatus.PROCESSED
158
+
159
+ def mark_failed(self) -> None:
160
+ """
161
+ Record a failed delivery attempt and advance status accordingly.
162
+
163
+ If the retry budget is not exhausted, status becomes RETRYING.
164
+ If the budget is exhausted, status becomes FAILED.
165
+ """
166
+ if self.retry_policy.is_exhausted(self.attempt_count):
167
+ self.status = DeliveryStatus.FAILED
168
+ else:
169
+ self.status = DeliveryStatus.RETRYING
170
+
171
+ def mark_skipped(self, reason: str = "") -> None:
172
+ """Record that this event was intentionally skipped (e.g., duplicate)."""
173
+ self.status = DeliveryStatus.SKIPPED
174
+ if reason:
175
+ self.tags["skip_reason"] = reason
176
+
177
+ def next_retry_wait_seconds(self) -> int:
178
+ """
179
+ Return the wait time before the next delivery attempt.
180
+
181
+ Returns 0 if the event is not in a retryable state.
182
+ """
183
+ if self.status != DeliveryStatus.RETRYING:
184
+ return 0
185
+ return self.retry_policy.wait_seconds_for_attempt(self.attempt_count)
186
+
187
+ def is_terminal(self) -> bool:
188
+ """Return True if the event has reached a state that requires no further processing."""
189
+ return self.status in {
190
+ DeliveryStatus.PROCESSED,
191
+ DeliveryStatus.FAILED,
192
+ DeliveryStatus.SKIPPED,
193
+ }
194
+
195
+ def to_audit_line(self) -> str:
196
+ """Format a structured log line for integration observability systems."""
197
+ return (
198
+ f"[INTEGRATION_EVENT] event_id={self.event_id} "
199
+ f"type={self.event_type} "
200
+ f"source={self.source_system} "
201
+ f"status={self.status.value} "
202
+ f"attempts={self.attempt_count} "
203
+ f"correlation={self.correlation_id or 'none'} "
204
+ f"schema={self.schema_version} "
205
+ f"created={self.created_at.isoformat()}"
206
+ )
@@ -0,0 +1,214 @@
1
+ """
2
+ sync_boundary.py — System-of-Record Synchronization Boundaries
3
+
4
+ Bi-directional synchronization between enterprise systems (CRM and ERP,
5
+ for example) fails in predictable ways. The most common failure is
6
+ ambiguity about which system is authoritative for a given field when
7
+ both systems have updated the same record simultaneously.
8
+
9
+ This module provides the structural primitives for making synchronization
10
+ boundaries explicit. It does not implement a full sync engine — it
11
+ provides the objects needed to define authority, detect conflicts, and
12
+ route resolution decisions to the right system.
13
+
14
+ The pattern applies wherever two or more enterprise platforms must
15
+ maintain consistent state for overlapping records: CRM + ERP,
16
+ CRM + SIS (Student Information System), ERP + ITSM, and similar pairings.
17
+ Platform-agnostic — the primitives work regardless of vendor.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from dataclasses import dataclass, field
23
+ from datetime import datetime, timezone
24
+ from enum import Enum
25
+ from typing import Any
26
+
27
+
28
+ class RecordAuthority(Enum):
29
+ """
30
+ Defines which system is authoritative for a given field or record type.
31
+
32
+ In bi-directional sync, a field must have exactly one authoritative system.
33
+ Changes in the authoritative system propagate outward; changes in
34
+ non-authoritative systems are either rejected, queued for review, or
35
+ overwritten on next sync.
36
+
37
+ SHARED indicates that authority is determined per-field (see SyncBoundary)
38
+ rather than per-record. Use this sparingly — field-level authority
39
+ increases complexity.
40
+ """
41
+ SYSTEM_A = "system_a" # Source system A is authoritative
42
+ SYSTEM_B = "system_b" # Source system B is authoritative
43
+ SHARED = "shared" # Field-level authority defined separately
44
+ MANUAL = "manual" # Human review required before sync
45
+
46
+
47
+ @dataclass(slots=True)
48
+ class SyncConflict:
49
+ """
50
+ Represents a detected conflict between two systems' versions of a field.
51
+
52
+ A conflict occurs when both systems have modified the same field since
53
+ the last successful sync. The sync boundary policy determines how to
54
+ resolve it: prefer one system, defer to manual review, or apply a
55
+ merge function.
56
+
57
+ Attributes:
58
+ field_name: The name of the field where the conflict was detected.
59
+ value_a: The value in system A at detection time.
60
+ value_b: The value in system B at detection time.
61
+ last_sync_value: The agreed-upon value from the last successful sync.
62
+ If None, this is the first sync for this record.
63
+ detected_at: Wall-clock time when the conflict was detected.
64
+ record_id: Identifier of the record where the conflict exists.
65
+ system_a_modified_at: When system A last modified this field.
66
+ None if not available.
67
+ system_b_modified_at: When system B last modified this field.
68
+ None if not available.
69
+ """
70
+ field_name: str
71
+ value_a: Any
72
+ value_b: Any
73
+ record_id: str
74
+ last_sync_value: Any = None
75
+ detected_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
76
+ system_a_modified_at: datetime | None = None
77
+ system_b_modified_at: datetime | None = None
78
+
79
+ def last_writer(self) -> RecordAuthority | None:
80
+ """
81
+ Return which system made the most recent modification, based on
82
+ available modification timestamps.
83
+
84
+ Returns None if timestamps are unavailable for one or both systems.
85
+ """
86
+ if self.system_a_modified_at is None or self.system_b_modified_at is None:
87
+ return None
88
+ if self.system_a_modified_at > self.system_b_modified_at:
89
+ return RecordAuthority.SYSTEM_A
90
+ if self.system_b_modified_at > self.system_a_modified_at:
91
+ return RecordAuthority.SYSTEM_B
92
+ return None # Same timestamp — cannot determine last writer
93
+
94
+
95
+ @dataclass
96
+ class SyncBoundary:
97
+ """
98
+ Defines the synchronization contract between two systems for one record type.
99
+
100
+ A SyncBoundary specifies:
101
+ 1. Which fields exist in both systems
102
+ 2. Which system is authoritative for each field
103
+ 3. Which fields should never be synced (exclusions)
104
+
105
+ Keeping this as an explicit data object — rather than encoding it in
106
+ sync scripts — makes the contract readable, testable, and auditable.
107
+
108
+ Usage::
109
+
110
+ boundary = SyncBoundary(
111
+ record_type="contact",
112
+ system_a_id="crm",
113
+ system_b_id="erp",
114
+ field_authority={
115
+ "email": RecordAuthority.SYSTEM_A,
116
+ "billing_address": RecordAuthority.SYSTEM_B,
117
+ "preferred_name": RecordAuthority.SYSTEM_A,
118
+ "account_status": RecordAuthority.SYSTEM_B,
119
+ },
120
+ excluded_fields={"internal_notes", "crm_lead_score"},
121
+ )
122
+
123
+ # Check if a field is synced and which system wins
124
+ authority = boundary.authority_for("email") # RecordAuthority.SYSTEM_A
125
+ is_synced = boundary.is_synced("internal_notes") # False
126
+
127
+ Attributes:
128
+ record_type: A name for the type of record this boundary governs.
129
+ system_a_id: Identifier for the first system (e.g., "crm", "salesforce", "hubspot").
130
+ system_b_id: Identifier for the second system (e.g., "erp", "peoplesoft", "sap").
131
+ field_authority: Maps field names to which system is authoritative.
132
+ excluded_fields: Fields that are never synchronized, even if present
133
+ in both systems.
134
+ allow_system_b_override: If True, system B changes to SYSTEM_A-authoritative
135
+ fields are queued for manual review rather than silently discarded.
136
+ Default False (system A changes simply overwrite system B).
137
+ """
138
+ record_type: str
139
+ system_a_id: str
140
+ system_b_id: str
141
+ field_authority: dict[str, RecordAuthority] = field(default_factory=dict)
142
+ excluded_fields: set[str] = field(default_factory=set)
143
+ allow_system_b_override: bool = False
144
+
145
+ def authority_for(self, field_name: str) -> RecordAuthority | None:
146
+ """
147
+ Return the authoritative system for the given field.
148
+
149
+ Returns None if the field is not in the sync boundary (either
150
+ excluded or not mapped).
151
+ """
152
+ if field_name in self.excluded_fields:
153
+ return None
154
+ return self.field_authority.get(field_name)
155
+
156
+ def is_synced(self, field_name: str) -> bool:
157
+ """Return True if this field is part of the synchronization boundary."""
158
+ return field_name not in self.excluded_fields and field_name in self.field_authority
159
+
160
+ def synced_fields(self) -> list[str]:
161
+ """Return the list of field names that are actively synchronized."""
162
+ return [f for f in self.field_authority if f not in self.excluded_fields]
163
+
164
+ def fields_owned_by(self, authority: RecordAuthority) -> list[str]:
165
+ """Return all fields where the given system is authoritative."""
166
+ return [
167
+ f for f, auth in self.field_authority.items()
168
+ if auth == authority and f not in self.excluded_fields
169
+ ]
170
+
171
+ def detect_conflict(
172
+ self,
173
+ field_name: str,
174
+ value_a: Any,
175
+ value_b: Any,
176
+ record_id: str,
177
+ last_sync_value: Any = None,
178
+ system_a_modified_at: datetime | None = None,
179
+ system_b_modified_at: datetime | None = None,
180
+ ) -> SyncConflict | None:
181
+ """
182
+ Detect whether a conflict exists for the given field.
183
+
184
+ A conflict exists when both systems have a value different from the
185
+ last sync value, indicating both have modified the field since the
186
+ last sync. If only one system differs from the last sync value,
187
+ that system made the authoritative change and no conflict exists.
188
+
189
+ Returns None if no conflict is detected or if the field is not synced.
190
+ """
191
+ if not self.is_synced(field_name):
192
+ return None
193
+
194
+ # Both systems agree — no conflict
195
+ if value_a == value_b:
196
+ return None
197
+
198
+ # If we have a last sync value, check whether both systems diverged
199
+ if last_sync_value is not None:
200
+ a_changed = value_a != last_sync_value
201
+ b_changed = value_b != last_sync_value
202
+ if not (a_changed and b_changed):
203
+ # Only one system changed — not a conflict
204
+ return None
205
+
206
+ return SyncConflict(
207
+ field_name=field_name,
208
+ value_a=value_a,
209
+ value_b=value_b,
210
+ record_id=record_id,
211
+ last_sync_value=last_sync_value,
212
+ system_a_modified_at=system_a_modified_at,
213
+ system_b_modified_at=system_b_modified_at,
214
+ )
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: integration-automation-patterns
3
+ Version: 0.1.0
4
+ Summary: Reference patterns for reliable enterprise integration, workflow automation, and system-of-record synchronization.
5
+ Author: Ashutosh Rana
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ashutoshrana/integration-automation-patterns
8
+ Project-URL: Issues, https://github.com/ashutoshrana/integration-automation-patterns/issues
9
+ Project-URL: Changelog, https://github.com/ashutoshrana/integration-automation-patterns/blob/main/CHANGELOG.md
10
+ Keywords: enterprise-integration,event-driven,workflow-automation,crm,erp,idempotency,system-of-record,integration-patterns,enterprise-architecture
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: System :: Distributed Computing
20
+ Classifier: Topic :: Office/Business
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: test
25
+ Requires-Dist: pytest>=7.0; extra == "test"
26
+ Requires-Dist: pytest-cov>=4.0; extra == "test"
27
+ Provides-Extra: lint
28
+ Requires-Dist: ruff>=0.4.0; extra == "lint"
29
+ Provides-Extra: typecheck
30
+ Requires-Dist: mypy>=1.0; extra == "typecheck"
31
+ Provides-Extra: dev
32
+ Requires-Dist: integration-automation-patterns[lint,test,typecheck]; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # integration-automation-patterns
36
+
37
+ [![CI](https://github.com/ashutoshrana/integration-automation-patterns/actions/workflows/ci.yml/badge.svg)](https://github.com/ashutoshrana/integration-automation-patterns/actions/workflows/ci.yml)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
39
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
40
+ [![PyPI](https://img.shields.io/pypi/v/integration-automation-patterns.svg)](https://pypi.org/project/integration-automation-patterns/)
41
+
42
+ Practical patterns for enterprise integration, workflow orchestration, and system-of-record synchronization in complex operating environments.
43
+
44
+ ## Why this repo exists
45
+
46
+ Enterprise modernization usually breaks down at the integration layer:
47
+ - brittle handoffs between systems
48
+ - inconsistent event handling
49
+ - weak retry and idempotency models
50
+ - workflow logic scattered across tools
51
+ - poor visibility into operational state
52
+
53
+ This repository is a public-safe reference for patterns that help teams build more reliable integration and automation systems. The patterns are platform-agnostic and cloud-agnostic — applicable across any combination of CRM, ERP, ITSM, and custom services, on any cloud environment (AWS, GCP, Azure, OCI) or on-premises.
54
+
55
+ ## Scope
56
+
57
+ This repo focuses on:
58
+ - event-driven integration patterns with explicit retry and idempotency models
59
+ - system-of-record synchronization with authority boundaries
60
+ - workflow orchestration and escalation boundaries
61
+ - observability for automation flows
62
+ - public-safe architecture notes for enterprise operations
63
+
64
+ The patterns do not assume any specific vendor, broker, or cloud platform.
65
+
66
+ ## Modules
67
+
68
+ - `event_envelope.py`
69
+ Reliable event transport with explicit delivery status, bounded retry policy,
70
+ and structured audit logging. Works with any message broker (Kafka, SQS,
71
+ Azure Service Bus, GCP Pub/Sub, RabbitMQ, IBM MQ, and others).
72
+
73
+ - `sync_boundary.py`
74
+ System-of-record synchronization contracts for bi-directional integration
75
+ between enterprise platforms. Explicit field-level authority assignment,
76
+ conflict detection, and exclusion management. Platform-agnostic.
77
+
78
+ ## Repository structure
79
+
80
+ - `src/integration_automation_patterns/`
81
+ - `event_envelope.py` — event transport with retry and audit
82
+ - `sync_boundary.py` — bi-directional sync authority boundaries
83
+ - `docs/architecture.md`
84
+ - `docs/implementation-note-01.md`
85
+ - `docs/adr/`
86
+ - `examples/event-flow.yaml`
87
+ - `CITATION.cff`
88
+ - `CONTRIBUTING.md`
89
+ - `GOVERNANCE.md`
90
+
91
+ ## Near-term roadmap
92
+
93
+ - add integration reliability ADRs
94
+ - add examples for retry-safe event handling across broker types
95
+ - document action logging and audit boundaries
96
+ - add workflow orchestration boundary patterns
97
+
98
+ ## Published notes
99
+
100
+ - implementation note: [`docs/implementation-note-01.md`](./docs/implementation-note-01.md)
101
+
102
+ ## Intended audience
103
+
104
+ - enterprise architects
105
+ - integration engineers
106
+ - workflow and automation operators
107
+ - platform teams responsible for system-of-record reliability across CRM, ERP, and service platforms
108
+
109
+ ## Citing this work
110
+
111
+ If you use these patterns in your work, see `CITATION.cff` or use GitHub's "Cite this repository" button above.
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/integration_automation_patterns/__init__.py
5
+ src/integration_automation_patterns/event_envelope.py
6
+ src/integration_automation_patterns/sync_boundary.py
7
+ src/integration_automation_patterns.egg-info/PKG-INFO
8
+ src/integration_automation_patterns.egg-info/SOURCES.txt
9
+ src/integration_automation_patterns.egg-info/dependency_links.txt
10
+ src/integration_automation_patterns.egg-info/requires.txt
11
+ src/integration_automation_patterns.egg-info/top_level.txt
12
+ tests/test_event_envelope.py
13
+ tests/test_sync_boundary.py
@@ -0,0 +1,13 @@
1
+
2
+ [dev]
3
+ integration-automation-patterns[lint,test,typecheck]
4
+
5
+ [lint]
6
+ ruff>=0.4.0
7
+
8
+ [test]
9
+ pytest>=7.0
10
+ pytest-cov>=4.0
11
+
12
+ [typecheck]
13
+ mypy>=1.0
@@ -0,0 +1,158 @@
1
+ """Tests for integration_automation_patterns.event_envelope."""
2
+
3
+ import pytest
4
+
5
+ from integration_automation_patterns.event_envelope import (
6
+ DeliveryStatus,
7
+ EventEnvelope,
8
+ RetryPolicy,
9
+ )
10
+
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # RetryPolicy
14
+ # ---------------------------------------------------------------------------
15
+
16
+ class TestRetryPolicy:
17
+ def test_default_values(self):
18
+ rp = RetryPolicy()
19
+ assert rp.max_attempts == 3
20
+ assert rp.backoff_seconds == 30
21
+ assert rp.exponential is True
22
+ assert rp.max_backoff_seconds == 3600
23
+
24
+ def test_is_exhausted_at_limit(self):
25
+ rp = RetryPolicy(max_attempts=3)
26
+ assert rp.is_exhausted(3) is True
27
+
28
+ def test_is_not_exhausted_below_limit(self):
29
+ rp = RetryPolicy(max_attempts=3)
30
+ assert rp.is_exhausted(2) is False
31
+
32
+ def test_fixed_backoff(self):
33
+ rp = RetryPolicy(backoff_seconds=10, exponential=False)
34
+ assert rp.wait_seconds_for_attempt(0) == 10
35
+ assert rp.wait_seconds_for_attempt(5) == 10
36
+
37
+ def test_exponential_backoff(self):
38
+ rp = RetryPolicy(backoff_seconds=10, exponential=True, max_backoff_seconds=9999)
39
+ assert rp.wait_seconds_for_attempt(0) == 10 # 10 * 2^0
40
+ assert rp.wait_seconds_for_attempt(1) == 20 # 10 * 2^1
41
+ assert rp.wait_seconds_for_attempt(2) == 40 # 10 * 2^2
42
+
43
+ def test_exponential_backoff_capped(self):
44
+ rp = RetryPolicy(backoff_seconds=10, exponential=True, max_backoff_seconds=50)
45
+ assert rp.wait_seconds_for_attempt(10) == 50 # capped at max
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # EventEnvelope lifecycle
50
+ # ---------------------------------------------------------------------------
51
+
52
+ def _make_event(**kwargs):
53
+ defaults = dict(
54
+ event_id="evt-001",
55
+ event_type="enrollment.student.updated",
56
+ source_system="crm",
57
+ payload={"student_id": "S-1"},
58
+ )
59
+ defaults.update(kwargs)
60
+ return EventEnvelope(**defaults)
61
+
62
+
63
+ class TestEventEnvelopeInitial:
64
+ def test_initial_status_pending(self):
65
+ ev = _make_event()
66
+ assert ev.status == DeliveryStatus.PENDING
67
+
68
+ def test_initial_attempt_count_zero(self):
69
+ ev = _make_event()
70
+ assert ev.attempt_count == 0
71
+
72
+ def test_is_not_terminal_when_pending(self):
73
+ ev = _make_event()
74
+ assert ev.is_terminal() is False
75
+
76
+ def test_fields_stored(self):
77
+ ev = _make_event(correlation_id="corr-99", schema_version="2.0")
78
+ assert ev.event_id == "evt-001"
79
+ assert ev.event_type == "enrollment.student.updated"
80
+ assert ev.source_system == "crm"
81
+ assert ev.correlation_id == "corr-99"
82
+ assert ev.schema_version == "2.0"
83
+
84
+
85
+ class TestEventEnvelopeTransitions:
86
+ def test_mark_dispatched_increments_attempt(self):
87
+ ev = _make_event()
88
+ ev.mark_dispatched()
89
+ assert ev.attempt_count == 1
90
+ assert ev.status == DeliveryStatus.DISPATCHED
91
+
92
+ def test_mark_acknowledged(self):
93
+ ev = _make_event()
94
+ ev.mark_dispatched()
95
+ ev.mark_acknowledged()
96
+ assert ev.status == DeliveryStatus.ACKNOWLEDGED
97
+
98
+ def test_mark_processed_is_terminal(self):
99
+ ev = _make_event()
100
+ ev.mark_dispatched()
101
+ ev.mark_processed()
102
+ assert ev.status == DeliveryStatus.PROCESSED
103
+ assert ev.is_terminal() is True
104
+
105
+ def test_mark_failed_within_budget_sets_retrying(self):
106
+ ev = _make_event()
107
+ ev.mark_dispatched() # attempt_count = 1
108
+ ev.mark_failed() # attempt_count=1 < max_attempts=3 → RETRYING
109
+ assert ev.status == DeliveryStatus.RETRYING
110
+ assert ev.is_terminal() is False
111
+
112
+ def test_mark_failed_budget_exhausted_sets_failed(self):
113
+ ev = _make_event()
114
+ for _ in range(3): # exhaust budget
115
+ ev.mark_dispatched()
116
+ ev.mark_failed()
117
+ assert ev.status == DeliveryStatus.FAILED
118
+ assert ev.is_terminal() is True
119
+
120
+ def test_mark_skipped_is_terminal(self):
121
+ ev = _make_event()
122
+ ev.mark_skipped(reason="duplicate")
123
+ assert ev.status == DeliveryStatus.SKIPPED
124
+ assert ev.is_terminal() is True
125
+ assert ev.tags["skip_reason"] == "duplicate"
126
+
127
+ def test_mark_skipped_without_reason(self):
128
+ ev = _make_event()
129
+ ev.mark_skipped()
130
+ assert ev.status == DeliveryStatus.SKIPPED
131
+ assert "skip_reason" not in ev.tags
132
+
133
+ def test_next_retry_wait_zero_when_not_retrying(self):
134
+ ev = _make_event()
135
+ assert ev.next_retry_wait_seconds() == 0
136
+
137
+ def test_next_retry_wait_positive_when_retrying(self):
138
+ ev = _make_event()
139
+ ev.mark_dispatched()
140
+ ev.mark_failed()
141
+ assert ev.next_retry_wait_seconds() > 0
142
+
143
+
144
+ class TestEventEnvelopeAudit:
145
+ def test_to_audit_line_contains_key_fields(self):
146
+ ev = _make_event(correlation_id="corr-1")
147
+ ev.mark_dispatched()
148
+ line = ev.to_audit_line()
149
+ assert "[INTEGRATION_EVENT]" in line
150
+ assert "event_id=evt-001" in line
151
+ assert "type=enrollment.student.updated" in line
152
+ assert "source=crm" in line
153
+ assert "correlation=corr-1" in line
154
+
155
+ def test_audit_line_no_correlation(self):
156
+ ev = _make_event()
157
+ line = ev.to_audit_line()
158
+ assert "correlation=none" in line
@@ -0,0 +1,197 @@
1
+ """Tests for integration_automation_patterns.sync_boundary."""
2
+
3
+ from datetime import datetime, timezone
4
+
5
+ import pytest
6
+
7
+ from integration_automation_patterns.sync_boundary import (
8
+ RecordAuthority,
9
+ SyncBoundary,
10
+ SyncConflict,
11
+ )
12
+
13
+
14
+ def _dt(offset_seconds: int = 0) -> datetime:
15
+ from datetime import timedelta
16
+ return datetime(2026, 1, 1, tzinfo=timezone.utc) + timedelta(seconds=offset_seconds)
17
+
18
+
19
+ def _boundary(**kwargs):
20
+ defaults = dict(
21
+ record_type="contact",
22
+ system_a_id="crm",
23
+ system_b_id="erp",
24
+ field_authority={
25
+ "email": RecordAuthority.SYSTEM_A,
26
+ "billing_address": RecordAuthority.SYSTEM_B,
27
+ "status": RecordAuthority.SYSTEM_A,
28
+ },
29
+ excluded_fields={"internal_notes"},
30
+ )
31
+ defaults.update(kwargs)
32
+ return SyncBoundary(**defaults)
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # SyncBoundary.authority_for
37
+ # ---------------------------------------------------------------------------
38
+
39
+ class TestSyncBoundaryAuthority:
40
+ def test_returns_authority_for_known_field(self):
41
+ b = _boundary()
42
+ assert b.authority_for("email") == RecordAuthority.SYSTEM_A
43
+ assert b.authority_for("billing_address") == RecordAuthority.SYSTEM_B
44
+
45
+ def test_returns_none_for_excluded_field(self):
46
+ b = _boundary()
47
+ assert b.authority_for("internal_notes") is None
48
+
49
+ def test_returns_none_for_unknown_field(self):
50
+ b = _boundary()
51
+ assert b.authority_for("unknown_field") is None
52
+
53
+ def test_is_synced_true_for_mapped_field(self):
54
+ b = _boundary()
55
+ assert b.is_synced("email") is True
56
+
57
+ def test_is_synced_false_for_excluded(self):
58
+ b = _boundary()
59
+ assert b.is_synced("internal_notes") is False
60
+
61
+ def test_is_synced_false_for_unmapped(self):
62
+ b = _boundary()
63
+ assert b.is_synced("no_such_field") is False
64
+
65
+ def test_synced_fields_excludes_excluded(self):
66
+ b = _boundary()
67
+ synced = b.synced_fields()
68
+ assert "internal_notes" not in synced
69
+ assert "email" in synced
70
+
71
+ def test_fields_owned_by_system_a(self):
72
+ b = _boundary()
73
+ owned = b.fields_owned_by(RecordAuthority.SYSTEM_A)
74
+ assert "email" in owned
75
+ assert "status" in owned
76
+ assert "billing_address" not in owned
77
+
78
+ def test_fields_owned_by_system_b(self):
79
+ b = _boundary()
80
+ owned = b.fields_owned_by(RecordAuthority.SYSTEM_B)
81
+ assert "billing_address" in owned
82
+ assert "email" not in owned
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # SyncBoundary.detect_conflict
87
+ # ---------------------------------------------------------------------------
88
+
89
+ class TestDetectConflict:
90
+ def test_no_conflict_when_values_equal(self):
91
+ b = _boundary()
92
+ result = b.detect_conflict("email", "a@x.com", "a@x.com", "rec-1")
93
+ assert result is None
94
+
95
+ def test_conflict_when_both_differ_from_last_sync(self):
96
+ b = _boundary()
97
+ conflict = b.detect_conflict(
98
+ field_name="email",
99
+ value_a="new_a@x.com",
100
+ value_b="new_b@x.com",
101
+ record_id="rec-1",
102
+ last_sync_value="old@x.com",
103
+ )
104
+ assert conflict is not None
105
+ assert isinstance(conflict, SyncConflict)
106
+ assert conflict.field_name == "email"
107
+
108
+ def test_no_conflict_when_only_a_changed(self):
109
+ b = _boundary()
110
+ result = b.detect_conflict(
111
+ field_name="email",
112
+ value_a="new_a@x.com",
113
+ value_b="old@x.com",
114
+ record_id="rec-1",
115
+ last_sync_value="old@x.com",
116
+ )
117
+ assert result is None
118
+
119
+ def test_no_conflict_when_only_b_changed(self):
120
+ b = _boundary()
121
+ result = b.detect_conflict(
122
+ field_name="email",
123
+ value_a="old@x.com",
124
+ value_b="new_b@x.com",
125
+ record_id="rec-1",
126
+ last_sync_value="old@x.com",
127
+ )
128
+ assert result is None
129
+
130
+ def test_conflict_without_last_sync_value(self):
131
+ b = _boundary()
132
+ conflict = b.detect_conflict("email", "a@x.com", "b@x.com", "rec-1")
133
+ assert conflict is not None
134
+
135
+ def test_no_conflict_for_excluded_field(self):
136
+ b = _boundary()
137
+ result = b.detect_conflict("internal_notes", "val_a", "val_b", "rec-1")
138
+ assert result is None
139
+
140
+ def test_no_conflict_for_unmapped_field(self):
141
+ b = _boundary()
142
+ result = b.detect_conflict("unknown_field", "val_a", "val_b", "rec-1")
143
+ assert result is None
144
+
145
+ def test_conflict_contains_correct_values(self):
146
+ b = _boundary()
147
+ conflict = b.detect_conflict(
148
+ "email", "a@x.com", "b@x.com", "rec-42", last_sync_value="old@x.com"
149
+ )
150
+ assert conflict.value_a == "a@x.com"
151
+ assert conflict.value_b == "b@x.com"
152
+ assert conflict.last_sync_value == "old@x.com"
153
+ assert conflict.record_id == "rec-42"
154
+
155
+
156
+ # ---------------------------------------------------------------------------
157
+ # SyncConflict.last_writer
158
+ # ---------------------------------------------------------------------------
159
+
160
+ class TestSyncConflictLastWriter:
161
+ def test_last_writer_a_wins(self):
162
+ c = SyncConflict(
163
+ field_name="email",
164
+ value_a="new",
165
+ value_b="old",
166
+ record_id="r1",
167
+ system_a_modified_at=_dt(100),
168
+ system_b_modified_at=_dt(50),
169
+ )
170
+ assert c.last_writer() == RecordAuthority.SYSTEM_A
171
+
172
+ def test_last_writer_b_wins(self):
173
+ c = SyncConflict(
174
+ field_name="email",
175
+ value_a="old",
176
+ value_b="new",
177
+ record_id="r1",
178
+ system_a_modified_at=_dt(50),
179
+ system_b_modified_at=_dt(100),
180
+ )
181
+ assert c.last_writer() == RecordAuthority.SYSTEM_B
182
+
183
+ def test_last_writer_none_when_timestamps_missing(self):
184
+ c = SyncConflict(field_name="email", value_a="a", value_b="b", record_id="r1")
185
+ assert c.last_writer() is None
186
+
187
+ def test_last_writer_none_when_same_timestamp(self):
188
+ ts = _dt(100)
189
+ c = SyncConflict(
190
+ field_name="email",
191
+ value_a="a",
192
+ value_b="b",
193
+ record_id="r1",
194
+ system_a_modified_at=ts,
195
+ system_b_modified_at=ts,
196
+ )
197
+ assert c.last_writer() is None