bb-integrations-library 3.0.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. bb_integrations_lib/__init__.py +0 -0
  2. bb_integrations_lib/converters/__init__.py +0 -0
  3. bb_integrations_lib/gravitate/__init__.py +0 -0
  4. bb_integrations_lib/gravitate/base_api.py +20 -0
  5. bb_integrations_lib/gravitate/model.py +29 -0
  6. bb_integrations_lib/gravitate/pe_api.py +122 -0
  7. bb_integrations_lib/gravitate/rita_api.py +552 -0
  8. bb_integrations_lib/gravitate/sd_api.py +572 -0
  9. bb_integrations_lib/gravitate/testing/TTE/sd/models.py +1398 -0
  10. bb_integrations_lib/gravitate/testing/TTE/sd/tests/test_models.py +2987 -0
  11. bb_integrations_lib/gravitate/testing/__init__.py +0 -0
  12. bb_integrations_lib/gravitate/testing/builder.py +55 -0
  13. bb_integrations_lib/gravitate/testing/openapi.py +70 -0
  14. bb_integrations_lib/gravitate/testing/util.py +274 -0
  15. bb_integrations_lib/mappers/__init__.py +0 -0
  16. bb_integrations_lib/mappers/prices/__init__.py +0 -0
  17. bb_integrations_lib/mappers/prices/model.py +106 -0
  18. bb_integrations_lib/mappers/prices/price_mapper.py +127 -0
  19. bb_integrations_lib/mappers/prices/protocol.py +20 -0
  20. bb_integrations_lib/mappers/prices/util.py +61 -0
  21. bb_integrations_lib/mappers/rita_mapper.py +523 -0
  22. bb_integrations_lib/models/__init__.py +0 -0
  23. bb_integrations_lib/models/dtn_supplier_invoice.py +487 -0
  24. bb_integrations_lib/models/enums.py +28 -0
  25. bb_integrations_lib/models/pipeline_structs.py +76 -0
  26. bb_integrations_lib/models/probe/probe_event.py +20 -0
  27. bb_integrations_lib/models/probe/request_data.py +431 -0
  28. bb_integrations_lib/models/probe/resume_token.py +7 -0
  29. bb_integrations_lib/models/rita/audit.py +113 -0
  30. bb_integrations_lib/models/rita/auth.py +30 -0
  31. bb_integrations_lib/models/rita/bucket.py +17 -0
  32. bb_integrations_lib/models/rita/config.py +188 -0
  33. bb_integrations_lib/models/rita/constants.py +19 -0
  34. bb_integrations_lib/models/rita/crossroads_entities.py +293 -0
  35. bb_integrations_lib/models/rita/crossroads_mapping.py +428 -0
  36. bb_integrations_lib/models/rita/crossroads_monitoring.py +78 -0
  37. bb_integrations_lib/models/rita/crossroads_network.py +41 -0
  38. bb_integrations_lib/models/rita/crossroads_rules.py +80 -0
  39. bb_integrations_lib/models/rita/email.py +39 -0
  40. bb_integrations_lib/models/rita/issue.py +63 -0
  41. bb_integrations_lib/models/rita/mapping.py +227 -0
  42. bb_integrations_lib/models/rita/probe.py +58 -0
  43. bb_integrations_lib/models/rita/reference_data.py +110 -0
  44. bb_integrations_lib/models/rita/source_system.py +9 -0
  45. bb_integrations_lib/models/rita/workers.py +76 -0
  46. bb_integrations_lib/models/sd/bols_and_drops.py +241 -0
  47. bb_integrations_lib/models/sd/get_order.py +301 -0
  48. bb_integrations_lib/models/sd/orders.py +18 -0
  49. bb_integrations_lib/models/sd_api.py +115 -0
  50. bb_integrations_lib/pipelines/__init__.py +0 -0
  51. bb_integrations_lib/pipelines/parsers/__init__.py +0 -0
  52. bb_integrations_lib/pipelines/parsers/distribution_report/__init__.py +0 -0
  53. bb_integrations_lib/pipelines/parsers/distribution_report/order_by_site_product_parser.py +50 -0
  54. bb_integrations_lib/pipelines/parsers/distribution_report/tank_configs_parser.py +47 -0
  55. bb_integrations_lib/pipelines/parsers/dtn/__init__.py +0 -0
  56. bb_integrations_lib/pipelines/parsers/dtn/dtn_price_parser.py +102 -0
  57. bb_integrations_lib/pipelines/parsers/dtn/model.py +79 -0
  58. bb_integrations_lib/pipelines/parsers/price_engine/__init__.py +0 -0
  59. bb_integrations_lib/pipelines/parsers/price_engine/parse_accessorials_prices_parser.py +67 -0
  60. bb_integrations_lib/pipelines/parsers/price_engine/price_file_upload/__init__.py +0 -0
  61. bb_integrations_lib/pipelines/parsers/price_engine/price_file_upload/price_merge_parser.py +111 -0
  62. bb_integrations_lib/pipelines/parsers/price_engine/price_file_upload/price_sync_parser.py +107 -0
  63. bb_integrations_lib/pipelines/parsers/price_engine/price_file_upload/shared.py +81 -0
  64. bb_integrations_lib/pipelines/parsers/tank_reading_parser.py +155 -0
  65. bb_integrations_lib/pipelines/parsers/tank_sales_parser.py +144 -0
  66. bb_integrations_lib/pipelines/shared/__init__.py +0 -0
  67. bb_integrations_lib/pipelines/shared/allocation_matching.py +227 -0
  68. bb_integrations_lib/pipelines/shared/bol_allocation.py +2793 -0
  69. bb_integrations_lib/pipelines/steps/__init__.py +0 -0
  70. bb_integrations_lib/pipelines/steps/create_accessorials_step.py +80 -0
  71. bb_integrations_lib/pipelines/steps/distribution_report/__init__.py +0 -0
  72. bb_integrations_lib/pipelines/steps/distribution_report/distribution_report_datafram_to_raw_data.py +33 -0
  73. bb_integrations_lib/pipelines/steps/distribution_report/get_model_history_step.py +50 -0
  74. bb_integrations_lib/pipelines/steps/distribution_report/get_order_by_site_product_step.py +62 -0
  75. bb_integrations_lib/pipelines/steps/distribution_report/get_tank_configs_step.py +40 -0
  76. bb_integrations_lib/pipelines/steps/distribution_report/join_distribution_order_dos_step.py +85 -0
  77. bb_integrations_lib/pipelines/steps/distribution_report/upload_distribution_report_datafram_to_big_query.py +47 -0
  78. bb_integrations_lib/pipelines/steps/echo_step.py +14 -0
  79. bb_integrations_lib/pipelines/steps/export_dataframe_to_rawdata_step.py +28 -0
  80. bb_integrations_lib/pipelines/steps/exporting/__init__.py +0 -0
  81. bb_integrations_lib/pipelines/steps/exporting/bbd_export_payroll_file_step.py +107 -0
  82. bb_integrations_lib/pipelines/steps/exporting/bbd_export_readings_step.py +236 -0
  83. bb_integrations_lib/pipelines/steps/exporting/cargas_wholesale_bundle_upload_step.py +33 -0
  84. bb_integrations_lib/pipelines/steps/exporting/dataframe_flat_file_export.py +29 -0
  85. bb_integrations_lib/pipelines/steps/exporting/gcs_bucket_export_file_step.py +34 -0
  86. bb_integrations_lib/pipelines/steps/exporting/keyvu_export_step.py +356 -0
  87. bb_integrations_lib/pipelines/steps/exporting/pe_price_export_step.py +238 -0
  88. bb_integrations_lib/pipelines/steps/exporting/platform_science_order_sync_step.py +500 -0
  89. bb_integrations_lib/pipelines/steps/exporting/save_rawdata_to_disk.py +15 -0
  90. bb_integrations_lib/pipelines/steps/exporting/sftp_export_file_step.py +60 -0
  91. bb_integrations_lib/pipelines/steps/exporting/sftp_export_many_files_step.py +23 -0
  92. bb_integrations_lib/pipelines/steps/exporting/update_exported_orders_table_step.py +64 -0
  93. bb_integrations_lib/pipelines/steps/filter_step.py +22 -0
  94. bb_integrations_lib/pipelines/steps/get_latest_sync_date.py +34 -0
  95. bb_integrations_lib/pipelines/steps/importing/bbd_import_payroll_step.py +30 -0
  96. bb_integrations_lib/pipelines/steps/importing/get_order_numbers_to_export_step.py +138 -0
  97. bb_integrations_lib/pipelines/steps/importing/load_file_to_dataframe_step.py +46 -0
  98. bb_integrations_lib/pipelines/steps/importing/load_imap_attachment_step.py +172 -0
  99. bb_integrations_lib/pipelines/steps/importing/pe_bulk_sync_price_structure_step.py +68 -0
  100. bb_integrations_lib/pipelines/steps/importing/pe_price_merge_step.py +86 -0
  101. bb_integrations_lib/pipelines/steps/importing/sftp_file_config_step.py +124 -0
  102. bb_integrations_lib/pipelines/steps/importing/test_exact_file_match.py +57 -0
  103. bb_integrations_lib/pipelines/steps/null_step.py +15 -0
  104. bb_integrations_lib/pipelines/steps/pe_integration_job_step.py +32 -0
  105. bb_integrations_lib/pipelines/steps/processing/__init__.py +0 -0
  106. bb_integrations_lib/pipelines/steps/processing/archive_gcs_step.py +76 -0
  107. bb_integrations_lib/pipelines/steps/processing/archive_sftp_step.py +48 -0
  108. bb_integrations_lib/pipelines/steps/processing/bbd_format_tank_readings_step.py +492 -0
  109. bb_integrations_lib/pipelines/steps/processing/bbd_upload_prices_step.py +54 -0
  110. bb_integrations_lib/pipelines/steps/processing/bbd_upload_tank_sales_step.py +124 -0
  111. bb_integrations_lib/pipelines/steps/processing/bbd_upload_tankreading_step.py +80 -0
  112. bb_integrations_lib/pipelines/steps/processing/convert_bbd_order_to_cargas_step.py +226 -0
  113. bb_integrations_lib/pipelines/steps/processing/delete_sftp_step.py +33 -0
  114. bb_integrations_lib/pipelines/steps/processing/dtn/__init__.py +2 -0
  115. bb_integrations_lib/pipelines/steps/processing/dtn/convert_dtn_invoice_to_sd_model.py +145 -0
  116. bb_integrations_lib/pipelines/steps/processing/dtn/parse_dtn_invoice_step.py +38 -0
  117. bb_integrations_lib/pipelines/steps/processing/file_config_parser_step.py +720 -0
  118. bb_integrations_lib/pipelines/steps/processing/file_config_parser_step_v2.py +418 -0
  119. bb_integrations_lib/pipelines/steps/processing/get_sd_price_price_request.py +105 -0
  120. bb_integrations_lib/pipelines/steps/processing/keyvu_upload_deliveryplan_step.py +39 -0
  121. bb_integrations_lib/pipelines/steps/processing/mark_orders_exported_in_bbd_step.py +185 -0
  122. bb_integrations_lib/pipelines/steps/processing/pe_price_rows_processing_step.py +174 -0
  123. bb_integrations_lib/pipelines/steps/processing/send_process_report_step.py +47 -0
  124. bb_integrations_lib/pipelines/steps/processing/sftp_renamer_step.py +61 -0
  125. bb_integrations_lib/pipelines/steps/processing/tank_reading_touchup_steps.py +75 -0
  126. bb_integrations_lib/pipelines/steps/processing/upload_supplier_invoice_step.py +16 -0
  127. bb_integrations_lib/pipelines/steps/send_attached_in_rita_email_step.py +44 -0
  128. bb_integrations_lib/pipelines/steps/send_rita_email_step.py +34 -0
  129. bb_integrations_lib/pipelines/steps/sleep_step.py +24 -0
  130. bb_integrations_lib/pipelines/wrappers/__init__.py +0 -0
  131. bb_integrations_lib/pipelines/wrappers/accessorials_transformation.py +104 -0
  132. bb_integrations_lib/pipelines/wrappers/distribution_report.py +191 -0
  133. bb_integrations_lib/pipelines/wrappers/export_tank_readings.py +237 -0
  134. bb_integrations_lib/pipelines/wrappers/import_tank_readings.py +192 -0
  135. bb_integrations_lib/pipelines/wrappers/wrapper.py +81 -0
  136. bb_integrations_lib/protocols/__init__.py +0 -0
  137. bb_integrations_lib/protocols/flat_file.py +210 -0
  138. bb_integrations_lib/protocols/gravitate_client.py +104 -0
  139. bb_integrations_lib/protocols/pipelines.py +697 -0
  140. bb_integrations_lib/provider/__init__.py +0 -0
  141. bb_integrations_lib/provider/api/__init__.py +0 -0
  142. bb_integrations_lib/provider/api/cargas/__init__.py +0 -0
  143. bb_integrations_lib/provider/api/cargas/client.py +43 -0
  144. bb_integrations_lib/provider/api/cargas/model.py +49 -0
  145. bb_integrations_lib/provider/api/cargas/protocol.py +23 -0
  146. bb_integrations_lib/provider/api/dtn/__init__.py +0 -0
  147. bb_integrations_lib/provider/api/dtn/client.py +128 -0
  148. bb_integrations_lib/provider/api/dtn/protocol.py +9 -0
  149. bb_integrations_lib/provider/api/keyvu/__init__.py +0 -0
  150. bb_integrations_lib/provider/api/keyvu/client.py +30 -0
  151. bb_integrations_lib/provider/api/keyvu/model.py +149 -0
  152. bb_integrations_lib/provider/api/macropoint/__init__.py +0 -0
  153. bb_integrations_lib/provider/api/macropoint/client.py +28 -0
  154. bb_integrations_lib/provider/api/macropoint/model.py +40 -0
  155. bb_integrations_lib/provider/api/pc_miler/__init__.py +0 -0
  156. bb_integrations_lib/provider/api/pc_miler/client.py +130 -0
  157. bb_integrations_lib/provider/api/pc_miler/model.py +6 -0
  158. bb_integrations_lib/provider/api/pc_miler/web_services_apis.py +131 -0
  159. bb_integrations_lib/provider/api/platform_science/__init__.py +0 -0
  160. bb_integrations_lib/provider/api/platform_science/client.py +147 -0
  161. bb_integrations_lib/provider/api/platform_science/model.py +82 -0
  162. bb_integrations_lib/provider/api/quicktrip/__init__.py +0 -0
  163. bb_integrations_lib/provider/api/quicktrip/client.py +52 -0
  164. bb_integrations_lib/provider/api/telapoint/__init__.py +0 -0
  165. bb_integrations_lib/provider/api/telapoint/client.py +68 -0
  166. bb_integrations_lib/provider/api/telapoint/model.py +178 -0
  167. bb_integrations_lib/provider/api/warren_rogers/__init__.py +0 -0
  168. bb_integrations_lib/provider/api/warren_rogers/client.py +207 -0
  169. bb_integrations_lib/provider/aws/__init__.py +0 -0
  170. bb_integrations_lib/provider/aws/s3/__init__.py +0 -0
  171. bb_integrations_lib/provider/aws/s3/client.py +126 -0
  172. bb_integrations_lib/provider/ftp/__init__.py +0 -0
  173. bb_integrations_lib/provider/ftp/client.py +140 -0
  174. bb_integrations_lib/provider/ftp/interface.py +273 -0
  175. bb_integrations_lib/provider/ftp/model.py +76 -0
  176. bb_integrations_lib/provider/imap/__init__.py +0 -0
  177. bb_integrations_lib/provider/imap/client.py +228 -0
  178. bb_integrations_lib/provider/imap/model.py +3 -0
  179. bb_integrations_lib/provider/sqlserver/__init__.py +0 -0
  180. bb_integrations_lib/provider/sqlserver/client.py +106 -0
  181. bb_integrations_lib/secrets/__init__.py +4 -0
  182. bb_integrations_lib/secrets/adapters.py +98 -0
  183. bb_integrations_lib/secrets/credential_models.py +222 -0
  184. bb_integrations_lib/secrets/factory.py +85 -0
  185. bb_integrations_lib/secrets/providers.py +160 -0
  186. bb_integrations_lib/shared/__init__.py +0 -0
  187. bb_integrations_lib/shared/exceptions.py +25 -0
  188. bb_integrations_lib/shared/model.py +1039 -0
  189. bb_integrations_lib/shared/shared_enums.py +510 -0
  190. bb_integrations_lib/storage/README.md +236 -0
  191. bb_integrations_lib/storage/__init__.py +0 -0
  192. bb_integrations_lib/storage/aws/__init__.py +0 -0
  193. bb_integrations_lib/storage/aws/s3.py +8 -0
  194. bb_integrations_lib/storage/defaults.py +72 -0
  195. bb_integrations_lib/storage/gcs/__init__.py +0 -0
  196. bb_integrations_lib/storage/gcs/client.py +8 -0
  197. bb_integrations_lib/storage/gcsmanager/__init__.py +0 -0
  198. bb_integrations_lib/storage/gcsmanager/client.py +8 -0
  199. bb_integrations_lib/storage/setup.py +29 -0
  200. bb_integrations_lib/util/__init__.py +0 -0
  201. bb_integrations_lib/util/cache/__init__.py +0 -0
  202. bb_integrations_lib/util/cache/custom_ttl_cache.py +75 -0
  203. bb_integrations_lib/util/cache/protocol.py +9 -0
  204. bb_integrations_lib/util/config/__init__.py +0 -0
  205. bb_integrations_lib/util/config/manager.py +391 -0
  206. bb_integrations_lib/util/config/model.py +41 -0
  207. bb_integrations_lib/util/exception_logger/__init__.py +0 -0
  208. bb_integrations_lib/util/exception_logger/exception_logger.py +146 -0
  209. bb_integrations_lib/util/exception_logger/test.py +114 -0
  210. bb_integrations_lib/util/utils.py +364 -0
  211. bb_integrations_lib/workers/__init__.py +0 -0
  212. bb_integrations_lib/workers/groups.py +13 -0
  213. bb_integrations_lib/workers/rpc_worker.py +50 -0
  214. bb_integrations_lib/workers/topics.py +20 -0
  215. bb_integrations_library-3.0.11.dist-info/METADATA +59 -0
  216. bb_integrations_library-3.0.11.dist-info/RECORD +217 -0
  217. bb_integrations_library-3.0.11.dist-info/WHEEL +4 -0
@@ -0,0 +1,428 @@
1
+ from enum import Enum
2
+ from functools import lru_cache
3
+ from pprint import pprint
4
+ from types import NoneType
5
+ from typing import List, Dict, Literal, Union, override, get_args
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+ from bb_integrations_lib.models.rita.crossroads_entities import CrossroadsCompany, CrossroadsProduct, \
10
+ CrossroadsTerminal, CrossroadsSite, CrossroadsTank, BaseCrossroadsEntity
11
+ from bb_integrations_lib.models.rita.crossroads_monitoring import CrossroadsMappingError
12
+ from bb_integrations_lib.shared.model import AgGridBaseModel
13
+
14
+
15
+ class CrossroadsMappingType(str, Enum):
16
+ """These are the types of records we support linking. """
17
+ location = "location"
18
+ product = "product"
19
+ site = "site"
20
+ tank = "tank"
21
+ counterparty = "counterparty"
22
+ terminal = "terminal"
23
+
24
+ def get_sd_collection(self):
25
+ if self in _sd_collections:
26
+ return _sd_collections[self]
27
+ else:
28
+ return None
29
+
30
+ @classmethod
31
+ def get_parent_types(self):
32
+ return _sd_collections.keys()
33
+
34
+ def get_sd_id_field(self):
35
+ if self not in _sd_id_fields:
36
+ return "id"
37
+ return _sd_id_fields[self]
38
+
39
+ def get_sd_name_field(self):
40
+ if self not in _sd_name_fields:
41
+ return "name"
42
+ return _sd_name_fields[self]
43
+
44
+ _sd_collections = {
45
+ CrossroadsMappingType.location: "location",
46
+ CrossroadsMappingType.product: "product",
47
+ CrossroadsMappingType.site: "store",
48
+ CrossroadsMappingType.counterparty: "counterparty",
49
+ CrossroadsMappingType.terminal: "location",
50
+ }
51
+
52
+ _sd_name_fields = {
53
+ CrossroadsMappingType.tank: "tank_id",
54
+ }
55
+
56
+ _sd_id_fields = {
57
+ CrossroadsMappingType.site: "store_number"
58
+ }
59
+
60
+
61
+ class CoreCrossroadsMapping(AgGridBaseModel):
62
+ """
63
+ An instance of this object associates a record in a source system to zero or more GOIDs. The data here lives only in
64
+ client tenants, not in the master tenant database. A CrossroadsMapping may be incomplete (matches no GOIDs), exact
65
+ (matches exactly 1 GOID), or multiple (matches 2+ GOIDs)
66
+ """
67
+ type: CrossroadsMappingType = Field(..., description="The type of record.")
68
+ goids: List[str] = Field([], description="Linked CrossroadsEntities by their GOIDs. A mapping may link to multiple CrossroadsEntities.")
69
+ display_name: str = Field(..., description="This is the friendly name of this record. It will by synced with the datasource if available.")
70
+ source_id: str = Field(..., description="Either (1) The Mongodb ID of the item in the tenant's S&D instance, or (2) The ID of the item in the specified source system.")
71
+ matching_info: dict = Field({}, description="This is info that we expect to use an AI agent to help us match records with CrossroadsEntities.")
72
+ extra_data: dict = Field({}, description="Additional data that may be used by mapping logic as needed.")
73
+
74
+
75
+
76
+ class CrossroadsMapping(CoreCrossroadsMapping):
77
+ source_system: str = Field("Gravitate Supply & Dispatch", description="The name of the system that this Mapping is for. Defaults to Gravitate Supply & Dispatch")
78
+ children: Dict[str, CoreCrossroadsMapping] = Field({}, description="Child mappings keyed by their source_id")
79
+
80
+
81
+ class CrossroadsMappingResult(AgGridBaseModel):
82
+ origin_tenant: str = ""
83
+ origin_source_id: str = ""
84
+ origin_source_system: str = ""
85
+ origin_crossroadsmapping: CrossroadsMapping = {}
86
+ target_tenant: str = ""
87
+ target_crossroadsmappings: list[CrossroadsMapping] = []
88
+ matched_crossroadsentities: dict[str, dict] = {}
89
+ milliseconds_taken: int | None = None
90
+
91
+
92
+ class MappingRequestData(BaseModel):
93
+ """
94
+ The body of a mapping request. A discriminated union. There is an is_resolved field that the backend uses to know if it can look in the non-id fields for more data.
95
+
96
+ AUTOMAGICAL: When defining a sublcass, any fields suffixed with _id will be auto-resolved by the Backend mapping methods.
97
+ The resolved data will be put into a field of the same name without the _id. E.g. `supplier_id` and `supplier` form a
98
+ pair that will be auto matched.
99
+ """
100
+ type: str
101
+ is_resolved: bool | None = Field(default=None, description="Set 'true' by the mapping engine if all of the id fields have been resolved. This is computed by backend; user input has no effect here.")
102
+
103
+ @classmethod
104
+ def automap_fields(cls) -> dict[str, str]:
105
+ """Returns a map with _id fields as the keys and full fields as the values. For use with getattr"""
106
+ id_fields = [f for f in cls.model_fields.keys() if f.endswith("_id")]
107
+ result = {}
108
+ for id_field_name in id_fields:
109
+ full_field_name = id_field_name.rstrip("_id")
110
+ if full_field_name in cls.model_fields:
111
+ result[id_field_name] = full_field_name
112
+ return result
113
+
114
+ @classmethod
115
+ def automap_child_fields(cls) -> dict[str, str]:
116
+ """Returns a map with _cid fields as the keys and full fields as the values. For use with getattr"""
117
+ cid_fields = [f for f in cls.model_fields.keys() if f.endswith("_cid")]
118
+ result = {}
119
+ for id_field_name in cid_fields:
120
+ full_field_name = id_field_name.rstrip("_cid")
121
+ if full_field_name in cls.model_fields:
122
+ result[id_field_name] = full_field_name
123
+ return result
124
+
125
+ @classmethod
126
+ def get_crossroads_entity_type(cls, full_field_name: str):
127
+ field = cls.model_fields[full_field_name]
128
+ for arg in get_args(field.annotation):
129
+ if arg == NoneType or issubclass(arg, CoreCrossroadsMapping):
130
+ continue
131
+ return arg
132
+
133
+ def resolve_child_mappings(self, tenant: str) -> None:
134
+ """Called after all parent mappings have been resolved. If there are children mappings, override this method and
135
+ implement a method to fetch them from the parent mappings. This method can
136
+ :param tenant: When an error is thrown, the provided tenant will be linked to the error.
137
+ """
138
+ pass
139
+
140
+ def get_parent_mapping(self, field_name: str) -> CrossroadsMapping:
141
+ """May be called by the mapping engine after all parent mappings are resolved. This method can be provided either
142
+ the ID field name or the full field name of a child mapping and should return the contents of the parent mapping object."""
143
+ pass
144
+
145
+ def get_mapping_type_for_field(self, field_name) -> CrossroadsMappingType | None:
146
+ """
147
+ Called when attempting to auto-resolve multiple mappings that have different types. This is a common case, so
148
+ we attempt to determine which data type we should use. E.g. if a crossroads entity is linked to 2 mappings, 1 site
149
+ and 1 location, this method should provide the way to disambiguate which one this property expects based on the field.
150
+ If the logic is more complicated than "always use this type for this mapping request" then this method is not the right
151
+ spot to implement that info; use custom rules instead.
152
+ """
153
+ return None
154
+
155
+ def access_by_property_name(self, property_name: str) -> str:
156
+ try:
157
+ path = property_name.split(".")
158
+ obj = self
159
+ for p in path:
160
+ obj = getattr(obj, p)
161
+ return str(obj)
162
+ except Exception as e:
163
+ return "<<<UNKNOWN>>>"
164
+
165
+ @classmethod
166
+ @lru_cache(maxsize=16)
167
+ def outgoing_rule_properties(cls) -> list[str]:
168
+ """
169
+ Returns the valid rule properties for this request data when considering outgoing rules. These properties can be used in conditionals.
170
+ Because they're restricted to outgoing rules you should return properties related to the crossroads mappings.
171
+ """
172
+ fields = list(cls.automap_fields().items()) + list(cls.automap_child_fields().items())
173
+ results = []
174
+ for _, full_field_name in fields:
175
+ results += [f"{full_field_name}.source_id", f"{full_field_name}.display_name"]
176
+ return results
177
+
178
+
179
+ @classmethod
180
+ @lru_cache(maxsize=16)
181
+ def incoming_rule_properties(cls) -> list[str]:
182
+ """
183
+ Returns the valid rule properties for this request data when considering outgoing rules. These properties can be used in conditionals.
184
+ Because they're restricted to outgoing rules you should return properties related to the crossroads mappings.
185
+ """
186
+ fields = list(cls.automap_fields().items()) + list(cls.automap_child_fields().items())
187
+ results = []
188
+ for _, full_field_name in fields:
189
+ pydantic_field = cls.model_fields[full_field_name]
190
+ for arg in get_args(pydantic_field.annotation):
191
+ if arg == NoneType or issubclass(arg, CoreCrossroadsMapping):
192
+ continue
193
+ props = list(arg.model_fields.keys())
194
+ props = [p for p in props if p != "is_active" and p != "record_owner"]
195
+ results += [f"{full_field_name}.{p}" for p in props]
196
+ return results
197
+
198
+
199
+ class BasicMapping(MappingRequestData):
200
+ """A basic mapping from one tenant to another. Rules are very limited here since there's very little data."""
201
+ type: Literal["basic_mapping"] = "basic_mapping"
202
+ obj_id: str
203
+ obj: Union[CrossroadsMapping, BaseCrossroadsEntity, None] = Field(default=None, description="If is_resolved == True, this will be the item pointed at by obj_id")
204
+ req_type: CrossroadsMappingType | None = None
205
+
206
+ @override
207
+ def get_mapping_type_for_field(self, field_name) -> CrossroadsMappingType | None:
208
+ if self.req_type is None:
209
+ return None
210
+ return self.req_type
211
+
212
+
213
+ class LoadPlan(MappingRequestData):
214
+ type: Literal["load_plan"] = "load_plan"
215
+ supplier_id: str
216
+ product_id: str
217
+ terminal_id: str
218
+ destination_id: str
219
+ destination_tank_cid: str
220
+ supplier: Union[CrossroadsMapping, CrossroadsCompany, None] = Field(default=None, description="If is_resolved == True, this will be the item pointed at by supplier_id")
221
+ product: Union[CrossroadsMapping, CrossroadsProduct, None] = Field(default=None, description="If is_resolved == True, this will be the item pointed at by product_id")
222
+ terminal: Union[CrossroadsMapping, CrossroadsTerminal, None] = Field(default=None, description="If is_resolved == True, this will be the item pointed at by terminal_id")
223
+ destination: Union[CrossroadsMapping, CrossroadsSite, None] = Field(default=None, description="If is_resolved == True, this will be the item pointed at by destination_id")
224
+ destination_tank: Union[CoreCrossroadsMapping, CrossroadsTank, None] = Field(default=None, description="If is_resolved == True, this will be the item pointed at tank_id")
225
+
226
+
227
+ @override
228
+ def get_mapping_type_for_field(self, field_name) -> CrossroadsMappingType | None:
229
+ if field_name.startswith("product"):
230
+ return CrossroadsMappingType.product
231
+ elif field_name.startswith("terminal"):
232
+ return CrossroadsMappingType.terminal
233
+ elif field_name.startswith("supplier"):
234
+ return CrossroadsMappingType.counterparty
235
+ elif field_name.startswith("destination_tank"):
236
+ return CrossroadsMappingType.tank
237
+ elif field_name.startswith("destination"):
238
+ return CrossroadsMappingType.site
239
+ raise ValueError(f"Invalid argument: {field_name}")
240
+
241
+ @override
242
+ def resolve_child_mappings(self, tenant: str) -> None:
243
+ if self.destination_tank_cid.startswith("tank:"):
244
+ tanks = [self.destination.children[t] for t in self.destination.children if self.destination_tank_cid in self.destination.children[t].goids and self.destination.children[t].type == CrossroadsMappingType.tank]
245
+ if len(tanks) != 1:
246
+ raise CrossroadsMappingError(
247
+ friendly_text=f"Error while resolving child mappings for destination_tank_cid = {self.destination_tank_cid}. Could not find a unique child of site {self.destination_id} that had {self.destination_tank_cid} as its GOID. Update the child mapping on the site to link to the Crossroads entity with this GOID.",
248
+ error="no link to child mapping", expected_fixes=["update child mapping"], tenant=tenant,
249
+ )
250
+ self.destination_tank_cid = tanks[0].source_id
251
+ self.destination_tank = tanks[0]
252
+ else:
253
+ self.destination_tank = self.destination.children.get(self.destination_tank_cid)
254
+ if not self.destination_tank:
255
+ raise CrossroadsMappingError(
256
+ friendly_text=f"Error while resolving child mappings for destination_tank_cid = {self.destination_tank_cid}. There was no child with source_id {self.destination_tank_cid} for the site {self.destination_id}. Create the child mapping on the site and link it to Crossroads.",
257
+ error="no child mapping", expected_fixes=["create child mapping"], tenant=tenant,
258
+ )
259
+
260
+ @override
261
+ def get_parent_mapping(self, field_name: str) -> CrossroadsMapping:
262
+ if field_name.startswith("destination_tank"):
263
+ return self.destination
264
+ raise ValueError("Invalid argument")
265
+
266
+
267
+ class SpecificSupplyDrop(MappingRequestData):
268
+ type: Literal["drop"] = "drop"
269
+ product_id: str
270
+ destination_id: str
271
+ destination_tank_cid: str
272
+ product: Union[CrossroadsMapping, CrossroadsProduct, None] = Field(default=None, description="If is_resolved == True, this will be the item pointed at by product_id")
273
+ destination: Union[CrossroadsMapping, CrossroadsSite, None] = Field(default=None, description="If is_resolved == True, this will be the item pointed at by destination_id")
274
+ destination_tank: Union[CoreCrossroadsMapping, CrossroadsTank, None] = Field(default=None, description="If is_resolved == True, this will be the item pointed at tank_id")
275
+
276
+ @override
277
+ def resolve_child_mappings(self, tenant: str) -> None:
278
+ if self.destination_tank_cid.startswith("tank:"):
279
+ tanks = [self.destination.children[t] for t in self.destination.children if self.destination_tank_cid in self.destination.children[t].goids and self.destination.children[t].type == CrossroadsMappingType.tank]
280
+ if len(tanks) != 1:
281
+ raise CrossroadsMappingError(
282
+ friendly_text=f"Error while resolving child mappings for destination_tank_cid = {self.destination_tank_cid}. Could not find a unique child of site {self.destination_id} that had {self.destination_tank_cid} as its GOID. Update the child mapping on the site to link to the Crossroads entity with this GOID.",
283
+ error="no link to child mapping", expected_fixes=["update child mapping"], tenant=tenant,
284
+ )
285
+ self.destination_tank_cid = tanks[0].source_id
286
+ self.destination_tank = tanks[0]
287
+ else:
288
+ self.destination_tank = self.destination.children.get(self.destination_tank_cid)
289
+ if not self.destination_tank:
290
+ raise CrossroadsMappingError(
291
+ friendly_text=f"Error while resolving child mappings for destination_tank_cid = {self.destination_tank_cid}. There was no child with source_id {self.destination_tank_cid} for the site {self.destination_id}. Create the child mapping on the site and link it to Crossroads.",
292
+ error="no child mapping", expected_fixes=["create child mapping"], tenant=tenant,
293
+ )
294
+
295
+ @override
296
+ def get_parent_mapping(self, field_name: str) -> CrossroadsMapping:
297
+ if field_name.startswith("destination_tank"):
298
+ return self.destination
299
+ raise ValueError("Invalid argument")
300
+
301
+ @override
302
+ def get_mapping_type_for_field(self, field_name) -> CrossroadsMappingType | None:
303
+ if field_name.startswith("product"):
304
+ return CrossroadsMappingType.product
305
+ elif field_name.startswith("supplier"):
306
+ return CrossroadsMappingType.counterparty
307
+ elif field_name.startswith("destination_tank"):
308
+ return CrossroadsMappingType.tank
309
+ elif field_name.startswith("destination"):
310
+ return CrossroadsMappingType.site
311
+ raise ValueError(f"Invalid argument: {field_name}")
312
+
313
+
314
+ class TSDDrop(MappingRequestData):
315
+ type: Literal["tsd_drop"] = "tsd_drop"
316
+ destination_id: str
317
+ destination_tank_cid: str
318
+ destination: Union[CrossroadsMapping, CrossroadsSite, None] = Field(default=None, description="If is_resolved == True, this will be the item pointed at by destination_id")
319
+ destination_tank: Union[CoreCrossroadsMapping, CrossroadsTank, None] = Field(default=None, description="If is_resolved == True, this will be the item pointed at tank_id")
320
+
321
+ @override
322
+ def resolve_child_mappings(self, tenant: str) -> None:
323
+ if self.destination_tank_cid.startswith("tank:"):
324
+ tanks = [self.destination.children[t] for t in self.destination.children if
325
+ self.destination_tank_cid in self.destination.children[t].goids and self.destination.children[t].type == CrossroadsMappingType.tank]
326
+ if len(tanks) != 1:
327
+ raise CrossroadsMappingError(
328
+ friendly_text=f"Error while resolving child mappings for destination_tank_cid = {self.destination_tank_cid}. Could not find a unique child of site {self.destination_id} that had {self.destination_tank_cid} as its GOID. Update the child mapping on the site to link to the Crossroads entity with this GOID.",
329
+ error="no link to child mapping", expected_fixes=["update child mapping"], tenant=tenant,
330
+ )
331
+ self.destination_tank_cid = tanks[0].source_id
332
+ self.destination_tank = tanks[0]
333
+ else:
334
+ self.destination_tank = self.destination.children.get(self.destination_tank_cid)
335
+ if not self.destination_tank:
336
+ raise CrossroadsMappingError(
337
+ friendly_text=f"Error while resolving child mappings for destination_tank_cid = {self.destination_tank_cid}. There was no child with source_id {self.destination_tank_cid} for the site {self.destination_id}. Create the child mapping on the site and link it to Crossroads.",
338
+ error="no child mapping", expected_fixes=["create child mapping"], tenant=tenant,
339
+ )
340
+
341
+ @override
342
+ def get_parent_mapping(self, field_name: str) -> CrossroadsMapping:
343
+ if field_name.startswith("destination_tank"):
344
+ return self.destination
345
+ raise ValueError("Invalid argument")
346
+
347
+ @override
348
+ def get_mapping_type_for_field(self, field_name) -> CrossroadsMappingType | None:
349
+ if field_name.startswith("destination_tank"):
350
+ return CrossroadsMappingType.tank
351
+ elif field_name.startswith("destination"):
352
+ return CrossroadsMappingType.site
353
+ raise ValueError(f"Invalid argument: {field_name}")
354
+
355
+
356
+ class TankReading(MappingRequestData):
357
+ type: Literal["tank_reading"] = "tank_reading"
358
+ site_id: str
359
+ tank_cid: str
360
+ site: Union[CrossroadsMapping, CrossroadsSite, None] = Field(default=None, description="If is_resolved == True, this will be the item pointed at site_id")
361
+ tank: Union[CoreCrossroadsMapping, CrossroadsTank, None] = Field(default=None, description="If is_resolved == True, this will be the item pointed at tank_id")
362
+
363
+ @override
364
+ def resolve_child_mappings(self, tenant: str) -> None:
365
+ if self.tank_cid.startswith("tank:"):
366
+ tanks = [self.site.children[t] for t in self.site.children if self.tank_cid in self.site.children[t].goids and self.site.children[t].type == CrossroadsMappingType.tank]
367
+ if len(tanks) != 1:
368
+ raise CrossroadsMappingError(
369
+ friendly_text=f"Error while resolving child mappings for tank_cid = {self.tank_cid}. Could not find a unique child of site {self.site_id} that had {self.tank_cid} as its GOID. Update the child mapping on the site to link to the Crossroads entity with this GOID.",
370
+ error="no link to child mapping", expected_fixes=["update child mapping"], tenant=tenant,
371
+ )
372
+ self.tank_cid = tanks[0].source_id
373
+ self.tank = tanks[0]
374
+ else:
375
+ self.tank = self.site.children.get(self.tank_cid)
376
+ if not self.tank:
377
+ raise CrossroadsMappingError(
378
+ friendly_text=f"Error while resolving child mappings for tank_cid = {self.tank_cid}. There was no child with source_id {self.tank_cid} for the site {self.site_id}. Create the child mapping on the site and link it to Crossroads.",
379
+ error="no child mapping", expected_fixes=["create child mapping"], tenant=tenant,
380
+ )
381
+
382
+ @override
383
+ def get_parent_mapping(self, field_name: str) -> CrossroadsMapping:
384
+ if field_name.startswith("tank"):
385
+ return self.site
386
+ raise ValueError("Invalid argument")
387
+
388
+ @override
389
+ def get_mapping_type_for_field(self, field_name: str) -> CrossroadsMappingType | None:
390
+ if field_name.startswith("site"):
391
+ return CrossroadsMappingType.site
392
+ elif field_name.startswith("tank"):
393
+ return CrossroadsMappingType.tank
394
+ raise ValueError(f"Invalid argument: {field_name}")
395
+
396
+
397
+ class OrderBasics(MappingRequestData):
398
+ type: Literal["order_basics"] = "order_basics"
399
+ supplier_id: str
400
+ supplier: Union[CrossroadsMapping, CrossroadsCompany, None] = Field(default=None, description="If is_resolved == True, this will be the item pointed at by supplier_id")
401
+
402
+ @override
403
+ def get_mapping_type_for_field(self, field_name: str) -> CrossroadsMappingType | None:
404
+ if field_name.startswith("supplier"):
405
+ return CrossroadsMappingType.counterparty
406
+ raise ValueError(f"Invalid argument: {field_name}")
407
+
408
+
409
+ class MappingRequest(BaseModel):
410
+ """These are 'fat' mapping requests: they request backend to map multiple fields but serve as their own context for
411
+ the rules engine."""
412
+ origin_tenant: str
413
+ origin_source_system: str
414
+ target_tenant: str
415
+ target_source_system: str
416
+
417
+ data: Union[
418
+ BasicMapping, LoadPlan, SpecificSupplyDrop, TSDDrop, TankReading, OrderBasics
419
+ ] = Field(..., discriminator="type", description="The body of the mapping request. See MappingRequestData and its subclasses.")
420
+
421
+ extra_data: dict = Field(default={}, description="Additional data that may be used by the mapping engine. Rules may also add fields here and read fields from here.")
422
+
423
+
424
+ class MappingRequestInternal(MappingRequest):
425
+ """Holding type to differentiate from a mapping request into or out of a tenant. Has a field to mark the difference"""
426
+ is_crossroads: Literal[True] = True
427
+
428
+
@@ -0,0 +1,78 @@
1
+ from datetime import datetime
2
+ from typing import Literal, Optional, Any, override, List
3
+
4
+ from pydantic import BaseModel, Field
5
+ from pydantic.dataclasses import dataclass
6
+
7
+ from bb_integrations_lib.models.rita.issue import IssueBase
8
+
9
+
10
+ @dataclass
11
+ class CrossroadsError(Exception):
12
+ issue: IssueBase = Field(..., description="An issue to be saved to logs")
13
+
14
+
15
+ @dataclass
16
+ class CrossroadsMappingError(Exception):
17
+ """Custom exception type that """
18
+ friendly_text: str = Field(..., description="User-readable text describing the error.")
19
+ error: str = Field(..., description="Error code")
20
+ expected_fixes: List[str] = Field(..., description="Expected fixes to the mapping problem")
21
+ tenant: str = Field(..., description="Tenant expected to resolve the issue.")
22
+ goid: str | None = Field(None, description="GOID of the offending crossroads entity. Usually set if the tenant needs to fix an incoming fanout.")
23
+ mapping_source_id: str | None = Field(None, description="The source ID of the offending mapping. Usually set if the tenant needs to fix an outgoing fanout")
24
+ mapping_source_system: str | None = Field(None, description="The source system of the offending mapping. Only if tenant != Gravitate")
25
+ mapping_display_name: str | None = Field(None, description="The display name of the offending mapping.")
26
+
27
+
28
+ class CrossroadsLog(BaseModel):
29
+ """A CrossroadsLog object describes the results of a crossroads operation."""
30
+ date: datetime = Field(description="Datetime the log was filed.")
31
+ status: Literal["succeeded", "failed", "pending", "unknown"] = Field(
32
+ description="Status of the logged operation. Creators should specify 'succeeded' 'failed' or 'pending'",
33
+ default="unknown")
34
+ tenant: str = Field(description="The tenant this record exists in.")
35
+ record_collection: str = Field(description="The DB collection this record belongs to.")
36
+ record_id: str = Field(description="The DB ID of the relevant record. Use with record_collection to look up data.")
37
+ record_id_field: str = Field(description="The DB field containing the record_id.")
38
+
39
+ target_tenant: str = Field(description="The tenant that the crossroads integration was targeting.")
40
+ target_record_collection: str | None = Field(default=None, description="The DB collection of the record in the target tenant.")
41
+ target_record_id: str | None = Field(default=None, description="The DB ID of the record in the target tenant.")
42
+ target_record_id_field: str | None = Field(default=None, description="The DB field containing the record_id.")
43
+
44
+ issue_key: Optional[str] = Field(
45
+ description="If status='failed', this should be set with a link to a Rita Issue describing the encountered problem.",
46
+ default=None)
47
+ issue: Optional[IssueBase] = Field(description="If status='failed', data provided here will be used to create the issue.", default=None)
48
+ is_mapping_failure: bool = Field(False, description="True if the reason for this log being 'failed' is a mapping error. If this is true, the mapping_error property will be set.")
49
+ mapping_error: CrossroadsMappingError | None = Field(default=None, description="Mapping error details. Set if mapping_error == True")
50
+ success_message: Optional[str] = Field(description="If status='success', this should be set with a message to show the user.", default=None)
51
+ success_details: Optional[dict] = Field(
52
+ description="If status='success', this should be set with additional details related to the successful integration. E.g. set the order number", default=None)
53
+ worker_name: str = Field(description="The name of the worker that created this log.")
54
+ connection_id: str | None = Field(None, description="The connection that generated this log.")
55
+ group: str | None = Field(None, description="Freeform group field; can be used as a display grouping for logs from multiple workers. E.g. 'TTE -> Caseys Crossroads' could group logs from many workers in the UI.")
56
+ worker_request: dict | None = Field(None, description="WIP. Plan is to use this to stash the worker request on a failure for retry capabilities.")
57
+ correlation_id: str | None = Field(default=None, description="The correlation ID for this log.")
58
+
59
+ def is_unspecific(self) -> bool:
60
+ """Returns true if this log is "Unspecific" and not attached to any record."""
61
+ return self.record_id == "unknown" and self.record_collection == "unknown" and self.record_id_field == "unknown"
62
+
63
+ @classmethod
64
+ def create_unspecific(cls, **kwargs):
65
+ kwargs = kwargs | {"record_id": "unknown", "record_collection": "unknown", "record_id_field": "unknown"}
66
+ return cls(**kwargs)
67
+
68
+
69
+ class OutputCrossroadsLog(CrossroadsLog):
70
+ correlation_id: str | None = Field(default=None, description="The correlation ID for this crossroads log.")
71
+
72
+
73
+ class MasterReferenceAudit(BaseModel):
74
+ """A snapshot of a MasterReferenceData or MasterReferenceLink object changed at a certain time. Creation and editing an MRD will update this object."""
75
+ document: Any = Field(description="A copy of the document as of the time of the change.")
76
+ doc_id: str = Field(description="MongoDB ID of the document in the relevant table.")
77
+ user: str = Field(description="The user that updated the document.")
78
+ date: datetime = Field(description="The datetime the document was updated.")
@@ -0,0 +1,41 @@
1
+ from typing import Optional
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class System(BaseModel):
7
+ """A System is a specific software product. Systems may or may not belong to a company in the crossroads network."""
8
+ name: str = Field(..., description="Name of the system")
9
+ description: Optional[str] = Field(None, description="Details")
10
+ is_erp: bool = Field(False, description="Does this sytem provide ERP functionality?")
11
+ is_tms: bool = Field(False, description="Does this sytem provide TMS functionality?")
12
+ is_active: bool = Field(True)
13
+
14
+
15
+ class Integration(BaseModel):
16
+ """An Integration describes a runnable integration that can be used in the crossroads network."""
17
+ name: str = Field(..., description="Name of the integration")
18
+ description: Optional[str] = Field(None, description="Detailed description of the integration.")
19
+ supersedes: Optional[str] = Field(None, description="ID of the integration that this integration supersedes. It is suggested to use this integration instead of the linked integration.")
20
+ config_schema: dict = Field(default_factory=dict, description="A JSON schema for the configuration that this integration expects.")
21
+ default_config: dict = Field(default_factory=dict, description="The default configuration values for this job.")
22
+ is_active: bool = Field(True)
23
+
24
+
25
+ class Node(BaseModel):
26
+ """A node is best thought of as a Company @ a System. So Caseys@Gravitate S+D, TTE@Gravitate S+D, Eagle@Telapoint are 3 nodes."""
27
+ company_id: str = Field(..., description="Mongodb ID of the company")
28
+ system_id: str = Field(..., description="Mongodb ID of the system")
29
+ is_phantom: bool = Field(False, description="Should this be shown as a phantom node?")
30
+ is_active: bool = Field(True)
31
+
32
+
33
+ class Connection(BaseModel):
34
+ """A connection is an instance of an integration. It points between two Nodes, references an integration to run, and specifies the config for this integration."""
35
+ name: str = Field(..., description="Name of the connection")
36
+ description: Optional[str] = Field(None, description="Detailed description of the connection")
37
+ origin_node_id: str = Field(..., description="ID of the origin node")
38
+ target_node_id: str = Field(..., description="ID of the target node")
39
+ integration_id: str = Field(..., description="ID of the integration")
40
+ config: dict = Field(..., description="Config of this instance of the integration. This must conform to the schema defined by that integration.")
41
+ is_active: bool = Field(True)
@@ -0,0 +1,80 @@
1
+ # Definitions and tech for the mapping rules engine.
2
+ from enum import Enum
3
+ from typing import Literal, runtime_checkable, Protocol
4
+
5
+ from bb_integrations_lib.models.rita.crossroads_mapping import MappingRequest
6
+ from pydantic import BaseModel, Field, model_validator, computed_field
7
+
8
+ from bb_integrations_lib.shared.model import AgGridBaseModel
9
+
10
+
11
+ class Condition(BaseModel):
12
+ """A single condition for a property or value in a mapping rule. A rule may have many conditions."""
13
+ property: str = Field(..., description="The property that this condition will inspect")
14
+ predicate: Literal["equals"] = Field("equals", description="The method of comparing the property and the value.")
15
+ value: str = Field(..., description="The value that this condition will inspect the property for, using the predicate.")
16
+
17
+
18
+ class Action(BaseModel):
19
+ type: Literal["choose result", "inject metadata"] = Field("choose result", description="Type of action")
20
+ value: str = Field(..., description="The value related to the type of action. This may be a GOID, a mapping ID, or a json string of metadata to provide to further mappings.")
21
+
22
+
23
+ class CrossroadsRule(AgGridBaseModel):
24
+ """Represents rules that can describe how to resolve mappings with more than one possible resolution. Rules are either
25
+ "incoming", "outgoing", or "override". Incoming rules describe how to choose a CrossroadsMapping when given a CrossroadsEntity.
26
+ Outgoing rules describe how to choose a CrossroadsEntity when given a CrossroadsMapping. Override rules describe how
27
+ to choose a CrossroadsMapping given a CrossroadsMapping, bypassing the CrossroadsEntities entirely.
28
+ """
29
+ display_name: str = Field("")
30
+ type: Literal["incoming", "outgoing", "override"] = Field("incoming", description="The type of this rule. Used to choose when a rule might apply.")
31
+ mapping_id: str | None = Field(None, description="If this rule is outgoing and related to a single mapping, then this field will be set with the DB ID of that mapping.")
32
+ entity_goid: str | None = Field(None, description="If this rule is incoming and related to a single master entity, then this field will be set with that entity's GOID.")
33
+ tenants: list[str] | None = Field(None, description="Which tenants this rule may apply to. If empty, can be any tenant. Must be set if type == override")
34
+ conditions: list[Condition] = Field([], description="The match conditions that make up this rule. Logical AND: A candidate must pass all conditions.")
35
+ action: Action = Field(..., description="The action taken by this rule when the match conditions are met.")
36
+ conditions_mode: Literal["and", "or"] = Field(default="and", description="How should multiple conditions be applied? In AND mode the conditions will be ANDed together logically. In OR mode the conditions will be ORed together logically.")
37
+ is_active: bool = Field(True)
38
+
39
+ @computed_field
40
+ @property
41
+ def requires_properties(self) -> list[str]:
42
+ return list({c.property for c in self.conditions})
43
+
44
+ def can_apply(self, req: MappingRequest):
45
+ if self.tenants is not None and len(self.tenants) > 0 and req.target_tenant not in self.tenants:
46
+ return False
47
+ for condition in self.conditions:
48
+ if self.type == "outgoing" and condition.property not in req.data.outgoing_rule_properties():
49
+ return False
50
+ elif self.type == "incoming" and condition.property not in req.data.incoming_rule_properties():
51
+ return False
52
+ return True
53
+
54
+ def meets_conditions(self, req: MappingRequest):
55
+ if self.conditions_mode == "and":
56
+ for condition in self.conditions:
57
+ request_value = req.data.access_by_property_name(condition.property)
58
+ if condition.predicate == "equals" and request_value != condition.value:
59
+ return False
60
+ return True # All conditions passed
61
+ else:
62
+ for conditions in self.conditions:
63
+ request_value = req.data.access_by_property_name(conditions.property)
64
+ if conditions.predicate == "equals" and request_value == conditions.value:
65
+ return True
66
+ return False # None of the conditions were met
67
+
68
+
69
+
70
+ @model_validator(mode="after")
71
+ def validate(self):
72
+ if self.mapping_id is not None and self.type != "outgoing":
73
+ raise ValueError("`mapping_id` should only be set when type is outgoing")
74
+ if self.entity_goid is not None and self.type != "incoming":
75
+ raise ValueError("`entity_goid` should only be set when type is incoming")
76
+ if not self.tenants and self.type == "override":
77
+ raise ValueError("`tenants` must be set when type is override")
78
+ return self
79
+
80
+