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,487 @@
1
+ import csv
2
+ from abc import abstractmethod, ABC
3
+ from datetime import datetime, date
4
+ from enum import StrEnum
5
+ from typing import Union, Optional, TextIO, Literal, Any, Generator
6
+
7
+ from pydantic import BaseModel, Field, conlist
8
+
9
+
10
+ class RecordType(StrEnum):
11
+ """Possible types of rows in DTN supplier invoice CSVs."""
12
+ HEADER = "BEGIN"
13
+ ITEM = "ITM"
14
+ RINS = "ITMRIN"
15
+ TAX = "ITMTAX"
16
+ SUMMARY_TAX = "SUMTAX"
17
+ DEFERRED_TAX = "DEFTAX"
18
+ FOOTER = "END"
19
+
20
+
21
+ class ParsableRow(ABC):
22
+ """Abstract class for parsable DTN CSV rows, additionally providing some common helper functions."""
23
+
24
+ @staticmethod
25
+ def _try_parse_date(date: str, format: str) -> date | None:
26
+ if date is None:
27
+ return None
28
+ try:
29
+ return datetime.strptime(date, format).date()
30
+ except ValueError:
31
+ return None
32
+
33
+ @staticmethod
34
+ def _try_parse_datetime(date: str, format: str) -> datetime | None:
35
+ try:
36
+ return datetime.strptime(date, format)
37
+ except ValueError:
38
+ return None
39
+
40
+ @staticmethod
41
+ def _optional_float(val: str) -> float | None:
42
+ return None if not val else float(val)
43
+
44
+ @staticmethod
45
+ def _pad_row(row: list, to: int) -> list:
46
+ row_copy = list(row)
47
+ if len(row_copy) < to:
48
+ row_copy.extend([None] * (to - len(row_copy)))
49
+ return row_copy
50
+
51
+ @staticmethod
52
+ def _parse_bool(val: str) -> bool:
53
+ match val:
54
+ case "Y":
55
+ return True
56
+ case "N":
57
+ return False
58
+ case _:
59
+ raise ValueError(f"Failed to parse boolean field: {val} is not Y or N")
60
+
61
+ @classmethod
62
+ @abstractmethod
63
+ def parse(cls, row: list[str]):
64
+ pass
65
+
66
+
67
+ class Header(ParsableRow, BaseModel):
68
+ """
69
+ Header row. Contains summary data for the invoice.
70
+ Only 1 allowed, per the file format specification (though we don't enforce this).
71
+ """
72
+ record_type: RecordType = RecordType.HEADER
73
+ dtn_transaction_number: str
74
+ dtn_version_number: str
75
+ transmission_datetime: datetime
76
+ invoice_number: str
77
+ invoice_date: date
78
+ document_type: str
79
+ seller_name: str
80
+ sold_to_name: str
81
+ sold_to_cust_no: Optional[str] = None
82
+ purchase_order_no: Optional[str] = None
83
+ terms_description: str
84
+ document_grand_total: float
85
+ invoice_due_date: Optional[date] = None
86
+ total_invoice_amount: float
87
+ discount_due_date: Optional[date] = None
88
+ discount: float
89
+ discount_amount: float
90
+ sender_id: str
91
+
92
+ @classmethod
93
+ def parse(cls, row: list[str]) -> "Header":
94
+ row = cls._pad_row(row, 20)
95
+ common_date_fmt = "%Y%m%d"
96
+ trans_datetime_str = row[3] + "T" + row[4]
97
+ trans_datetime = datetime.strptime(trans_datetime_str, "%Y%m%dT%H%M")
98
+
99
+ return cls(
100
+ dtn_transaction_number=row[1],
101
+ dtn_version_number=row[2],
102
+ transmission_datetime=trans_datetime,
103
+ invoice_number=row[5],
104
+ invoice_date=cls._try_parse_date(row[6], common_date_fmt),
105
+ document_type=row[7],
106
+ seller_name=row[8],
107
+ sold_to_name=row[9],
108
+ sold_to_cust_no=row[10],
109
+ purchase_order_no=row[11],
110
+ terms_description=row[12],
111
+ document_grand_total=float(row[13]),
112
+ invoice_due_date=cls._try_parse_date(row[14], common_date_fmt),
113
+ total_invoice_amount=cls._optional_float(row[15]),
114
+ discount_due_date=cls._try_parse_date(row[16], common_date_fmt),
115
+ discount=cls._optional_float(row[17]),
116
+ discount_amount=cls._optional_float(row[18]),
117
+ sender_id=row[19],
118
+ )
119
+
120
+
121
+ class BilledQuantityIndicator(StrEnum):
122
+ NET = "N"
123
+ GROSS = "G"
124
+ UNKNOWN = "U"
125
+
126
+
127
+ class Item(ParsableRow, BaseModel):
128
+ """Inventory item data row."""
129
+ record_type: RecordType = RecordType.ITEM
130
+ invoice_number: str
131
+ bol_number: Optional[str] = None
132
+ description: str
133
+ dtn_product_code: Optional[str] = None
134
+ supplier_product_code: Optional[str] = None
135
+ quantity_billed: float
136
+ billed_quantity_indicator: BilledQuantityIndicator
137
+ gross_quantity: Optional[float] = None
138
+ net_quantity: Optional[float] = None
139
+ unit_of_measure: str
140
+ rate: Optional[float] = None
141
+ line_total: float
142
+ ship_datetime: Optional[datetime] = None
143
+ ship_from_name: str
144
+ ship_from_address: Optional[str] = None
145
+ ship_from_address_2: Optional[str] = None
146
+ ship_from_city: Optional[str] = None
147
+ ship_from_state: Optional[str] = None
148
+ ship_from_zip: Optional[str] = None
149
+ dtn_splc: Optional[str] = None
150
+ ship_to_number: Optional[str] = None
151
+ ship_to_address: Optional[str] = None
152
+ ship_to_address_2: Optional[str] = None
153
+ ship_to_city: Optional[str] = None
154
+ ship_to_state: Optional[str] = None
155
+ ship_to_zip: Optional[str] = None
156
+ carrier_description: Optional[str] = None
157
+ carrier_fein_number: Optional[str] = None
158
+ original_invoice_number: Optional[str] = None
159
+ contract_number: Optional[str] = None
160
+ order_number: Optional[str] = None
161
+ vehicle_or_tank_number: Optional[str] = None
162
+ rins_records: Optional[list["ItemRins"]] = None
163
+ tax_records: Optional[list["ItemTax"]] = None
164
+
165
+ def add_rins_record(self, rins_record: "ItemRins") -> None:
166
+ if self.rins_records is None:
167
+ self.rins_records = []
168
+ self.rins_records.append(rins_record)
169
+
170
+ def add_tax_record(self, tax_record: "ItemTax") -> None:
171
+ if self.tax_records is None:
172
+ self.tax_records = []
173
+ self.tax_records.append(tax_record)
174
+
175
+ @classmethod
176
+ def parse(cls, row: list[str]) -> "Item":
177
+ row = cls._pad_row(row, 34)
178
+ common_date_fmt = "%Y%m%d"
179
+ # Note that this is the only field in the invoice where date/time are both optional.
180
+ # If the date is unavailable, time is useless, so we return None.
181
+ # If time is not available, we can still return a datetime with hours/mins/seconds defaulted to 0.
182
+ # If both are available, we'll parse them.
183
+ date_str = row[13]
184
+ time_str = row[14]
185
+ if not date_str:
186
+ ship_datetime = None
187
+ elif date_str and not time_str:
188
+ ship_datetime = datetime.strptime(date_str, common_date_fmt)
189
+ else:
190
+ ship_datetime = datetime.strptime(date_str + "T" + time_str, f"{common_date_fmt}T%H%M")
191
+ return cls(
192
+ invoice_number=row[1],
193
+ bol_number=row[2],
194
+ description=row[3],
195
+ dtn_product_code=row[4],
196
+ supplier_product_code=row[5],
197
+ quantity_billed=cls._optional_float(row[6]),
198
+ billed_quantity_indicator=BilledQuantityIndicator(row[7]),
199
+ gross_quantity=cls._optional_float(row[8]),
200
+ net_quantity=cls._optional_float(row[9]),
201
+ unit_of_measure=row[10],
202
+ rate=cls._optional_float(row[11]),
203
+ line_total=float(row[12]),
204
+ ship_datetime=ship_datetime,
205
+ ship_from_name=row[15],
206
+ ship_from_address=row[16],
207
+ ship_from_address_2=row[17],
208
+ ship_from_city=row[18],
209
+ ship_from_state=row[19],
210
+ ship_from_zip=row[20],
211
+ dtn_splc=row[21],
212
+ ship_to_number=row[22],
213
+ ship_to_address=row[23],
214
+ ship_to_address_2=row[24],
215
+ ship_to_city=row[25],
216
+ ship_to_state=row[26],
217
+ ship_to_zip=row[27],
218
+ carrier_description=row[28],
219
+ carrier_fein_number=row[29],
220
+ original_invoice_number=row[30],
221
+ contract_number=row[31],
222
+ order_number=row[32],
223
+ vehicle_or_tank_number=row[33],
224
+ )
225
+
226
+
227
+ class ItemRins(ParsableRow, BaseModel):
228
+ """EPA Renewable Information Number record row - follows a specific invoice item, but may be omitted."""
229
+ record_type: RecordType = RecordType.RINS
230
+ rins: str
231
+ supporting_document_number: Optional[str] = None
232
+ reserved_1: str
233
+ reserved_2: str
234
+ reserved_3: str
235
+
236
+ @classmethod
237
+ def parse(cls, row: list[str]) -> "ItemRins":
238
+ row = cls._pad_row(row, 6)
239
+ return cls(
240
+ rins=row[1],
241
+ supporting_document_number=row[2],
242
+ reserved_1=row[3],
243
+ reserved_2=row[4],
244
+ reserved_3=row[5],
245
+ )
246
+
247
+
248
+ class ItemTax(ParsableRow, BaseModel):
249
+ """Tax record for the preceding item. May be omitted, and may occur up to 100 times."""
250
+ record_type: RecordType = RecordType.TAX
251
+ invoice_number: str
252
+ bol_number: Optional[str] = None
253
+ description: str
254
+ quantity_billed: Optional[float] = None
255
+ unit_of_measure: Optional[str] = None
256
+ deferred: bool
257
+ rate: Optional[float] = None
258
+ line_total: float
259
+ reserved: Optional[str] = None
260
+ tax_code: Optional[str] = None
261
+ deferred_due_date: Optional[date] = None
262
+ deferred_invoice_number: Optional[str] = None
263
+
264
+ @classmethod
265
+ def parse(cls, row: list[str]) -> "ItemTax":
266
+ row = cls._pad_row(row, 13)
267
+ common_date_fmt = "%Y%m%d"
268
+ return cls(
269
+ invoice_number=row[1],
270
+ bol_number=row[2],
271
+ description=row[3],
272
+ quantity_billed=cls._optional_float(row[4]),
273
+ unit_of_measure=row[5],
274
+ deferred=cls._parse_bool(row[6]),
275
+ rate=cls._optional_float(row[7]),
276
+ line_total=cls._optional_float(row[8]),
277
+ reserved=row[9],
278
+ tax_code=row[10],
279
+ deferred_due_date=cls._try_parse_date(row[11], common_date_fmt),
280
+ deferred_invoice_number=row[12],
281
+ )
282
+
283
+
284
+ class SummaryTax(ParsableRow, BaseModel):
285
+ """Summary tax invoice rows."""
286
+ record_type: RecordType = RecordType.SUMMARY_TAX
287
+ invoice_number: str
288
+ description: str
289
+ quantity_billed: Optional[float] = None
290
+ unit_of_measure: Optional[str] = None
291
+ deferred: bool
292
+ rate: Optional[float] = None
293
+ line_total: float
294
+ tax_code: Optional[str] = None
295
+ deferred_due_date: Optional[date] = None
296
+ deferred_invoice_number: Optional[str] = None
297
+
298
+ @classmethod
299
+ def parse(cls, row: list[str]) -> "SummaryTax":
300
+ row = cls._pad_row(row, 11)
301
+ common_date_fmt = "%Y%m%d"
302
+ return cls(
303
+ invoice_number=row[1],
304
+ description=row[2],
305
+ quantity_billed=cls._optional_float(row[3]),
306
+ unit_of_measure=row[4],
307
+ deferred=cls._parse_bool(row[5]),
308
+ rate=cls._optional_float(row[6]),
309
+ line_total=float(row[7]),
310
+ tax_code=row[8],
311
+ deferred_due_date=cls._try_parse_date(row[9], common_date_fmt),
312
+ deferred_invoice_number=row[10]
313
+ )
314
+
315
+
316
+ class DeferredTaxItem(BaseModel):
317
+ index: int
318
+ amount: Optional[float] = None
319
+ description: Optional[Literal["STATE", "FEDERAL", "UNKNOWN"]] = None
320
+ deferred_date: Optional[date] = None
321
+ deferred_invoice_number: Optional[str] = None
322
+
323
+ @property
324
+ def all_none(self) -> bool:
325
+ return (
326
+ self.amount is None
327
+ and self.description is None
328
+ and self.deferred_date is None
329
+ and self.deferred_invoice_number is None
330
+ )
331
+
332
+
333
+ class DeferredTax(ParsableRow, BaseModel):
334
+ """Deferred tax invoice row. Only expected once per invoice."""
335
+ record_type: RecordType = RecordType.DEFERRED_TAX
336
+ items: conlist(DeferredTaxItem, min_length=1)
337
+
338
+ @classmethod
339
+ def parse(cls, row: list[str]) -> "DeferredTax":
340
+ row = cls._pad_row(row, 41)
341
+ common_date_fmt = "%Y%m%d"
342
+
343
+ # Iterate over the groups of columns of data that represent each item and extract them
344
+ items = []
345
+ chunked = [row[i:i + 4] for i in range(1, 41, 4)]
346
+ index = 1
347
+ # We pad the row with blank spaces which do not parse as Optionals.
348
+ # 'or None' converts them to a NoneType so they do.
349
+ for group in chunked:
350
+ items.append(
351
+ DeferredTaxItem(
352
+ index=index,
353
+ amount=cls._optional_float(group[0]),
354
+ description=group[1] or None,
355
+ deferred_date=cls._try_parse_date(group[2], common_date_fmt),
356
+ deferred_invoice_number=group[3] or None,
357
+ )
358
+ )
359
+ index += 1
360
+ if len(items) < 1:
361
+ raise InvalidDTNInvoiceException("Malformed deferred tax item - must have at least 1 entry")
362
+ items = list(filter(lambda x: not x.all_none, items))
363
+ return DeferredTax(
364
+ items=items
365
+ )
366
+
367
+
368
+ class Footer(ParsableRow, BaseModel):
369
+ """Footer row. Indicates the record is complete."""
370
+ record_type: RecordType = RecordType.FOOTER
371
+ dtn_transaction_number: str
372
+ record_count: int
373
+
374
+ @classmethod
375
+ def parse(cls, row: list[str]) -> "Footer":
376
+ return cls(
377
+ dtn_transaction_number=row[1],
378
+ record_count=int(row[2]),
379
+ )
380
+
381
+
382
+ CSVRow: Union[Header, Item, ItemRins, ItemTax, SummaryTax, DeferredTax, Footer] = Field(discriminator="record_type")
383
+
384
+
385
+ class DTNSupplierInvoice(BaseModel):
386
+ header: Header
387
+ items: list[Item]
388
+ summary_taxes: list[SummaryTax]
389
+ deferred_taxes: Optional[DeferredTax] = None
390
+ footer: Footer
391
+ original_filename: Optional[str] = None
392
+
393
+
394
+ class InvalidDTNInvoiceException(BaseException):
395
+ pass
396
+
397
+
398
+ class Parser:
399
+ _record_cls = {
400
+ RecordType.HEADER: Header,
401
+ RecordType.ITEM: Item,
402
+ RecordType.RINS: ItemRins,
403
+ RecordType.TAX: ItemTax,
404
+ RecordType.SUMMARY_TAX: SummaryTax,
405
+ RecordType.DEFERRED_TAX: DeferredTax,
406
+ RecordType.FOOTER: Footer,
407
+ }
408
+
409
+ def __init__(self):
410
+ pass
411
+
412
+ def _parse_row(self, row: list[str]) -> CSVRow:
413
+ record_type = RecordType(row[0])
414
+ return self._record_cls[record_type].parse(row)
415
+
416
+ def _map_rows_by_type(self, rows: list[CSVRow]) -> dict[RecordType, list[CSVRow]]:
417
+ types_map = {k: [] for k in self._record_cls.keys()}
418
+ for row in rows:
419
+ types_map[row.record_type].append(row)
420
+ return types_map
421
+
422
+ def parse_all(self, csv_file: TextIO) -> Generator[DTNSupplierInvoice, Any, None]:
423
+ reader = csv.reader(csv_file, delimiter=",")
424
+ abs_line = 0
425
+ while True: # Until EOF is reached (StopIteration raised from next(reader) call)
426
+ header = None
427
+ footer = None
428
+ items = []
429
+ summary_taxes = []
430
+ deferred_taxes = None
431
+ last_item: Item | None = None
432
+ line = 0
433
+ while footer is None:
434
+ abs_line += 1
435
+ try:
436
+ row = next(reader)
437
+ except StopIteration:
438
+ # If we've reached EOF but parsed any number of lines for an additional invoice, then the file is
439
+ # malformed.
440
+ if line > 0:
441
+ raise InvalidDTNInvoiceException("Unexpected EOF")
442
+ return
443
+ line += 1
444
+ parsed = self._parse_row(row)
445
+ if line > 1 and header is None:
446
+ raise InvalidDTNInvoiceException(f"Header row not found before data rows started")
447
+ match parsed.record_type:
448
+ case RecordType.HEADER:
449
+ if header is not None:
450
+ raise InvalidDTNInvoiceException(
451
+ f"Only one header row is allowed (got another at line {abs_line})")
452
+ header = parsed
453
+ case RecordType.FOOTER:
454
+ if footer is not None:
455
+ raise InvalidDTNInvoiceException(
456
+ f"Only one footer row is allowed (got another at line {abs_line})")
457
+ footer = parsed
458
+ case RecordType.ITEM:
459
+ items.append(parsed)
460
+ last_item = parsed
461
+ case RecordType.RINS:
462
+ if last_item is None:
463
+ raise InvalidDTNInvoiceException(
464
+ f"Unattached RINS record at line {abs_line} (encountered before any invoice items were)")
465
+ last_item.add_rins_record(parsed)
466
+ case RecordType.TAX:
467
+ if last_item is None:
468
+ raise InvalidDTNInvoiceException(
469
+ f"Unattached item tax record at line {abs_line} (encountered before any invoice items were)")
470
+ last_item.add_tax_record(parsed)
471
+ case RecordType.SUMMARY_TAX:
472
+ summary_taxes.append(parsed)
473
+ case RecordType.DEFERRED_TAX:
474
+ if deferred_taxes is not None:
475
+ raise InvalidDTNInvoiceException(
476
+ f"Only one deferred tax row is allowed (got another at line {abs_line})")
477
+ deferred_taxes = parsed
478
+ yield DTNSupplierInvoice(
479
+ header=header,
480
+ items=items,
481
+ summary_taxes=summary_taxes,
482
+ deferred_taxes=deferred_taxes,
483
+ footer=footer,
484
+ )
485
+
486
+ def parse(self, csv_file: TextIO) -> list[DTNSupplierInvoice]:
487
+ return list(self.parse_all(csv_file))
@@ -0,0 +1,28 @@
1
+ from enum import Enum
2
+
3
+
4
+ class ProbeEventType(str, Enum):
5
+ create = "create"
6
+ update = "update"
7
+ delete = "delete"
8
+
9
+
10
+ class CrossroadsEntityType(str, Enum):
11
+ """Supported entities for crossroads integrations."""
12
+ # For watching orders in a generic manner (i.e. report when created). Planned for use with Telapoint integration
13
+ order = "order"
14
+
15
+ # For watching orders specifically for the carrier order integration (which will only be kicked off when an order is assigned to a carrier)
16
+ carrier_order = "carrier order integration"
17
+
18
+
19
+ class CrossroadsTaskStatus(str, Enum):
20
+ created = "created"
21
+ started = "started"
22
+ succeeded = "succeeded"
23
+ failed = "failed"
24
+
25
+
26
+ class ConnectorAction(str, Enum):
27
+ carrier_integration_create_order = "carrier integration - create order for carrier"
28
+ carrier_integration_update_order_status = "carrier integration - update order status for customer"
@@ -0,0 +1,76 @@
1
+ from typing import Any, List, Dict, Optional
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from bb_integrations_lib.models.rita.config import FileConfig, MaxSync
6
+ from bb_integrations_lib.models.rita.issue import IssueBase
7
+ from bb_integrations_lib.protocols.flat_file import Integration
8
+ from bb_integrations_lib.secrets import IntegrationSecretProvider
9
+
10
+
11
+ class StopBranch(Exception):
12
+ pass
13
+
14
+ class StopPipeline(Exception):
15
+ pass
16
+
17
+ class NoPipelineData(Exception):
18
+ """
19
+ To be raised when a pipeline step is unable to find any data to operate on.
20
+ This would cause an error alert.
21
+ A.K.A hard alert/exception.
22
+ """
23
+ pass
24
+
25
+ class NoPipelineSourceData(Exception):
26
+ """
27
+ To be raised when a pipeline step is unable to find any data to operate on.
28
+ This would cause an error alert.
29
+ A.K.A hard alert/exception.
30
+ """
31
+ pass
32
+
33
+ class UploadResult(BaseModel):
34
+ succeeded: int = 0
35
+ failed: int = 0
36
+ succeeded_items: list = []
37
+
38
+ class BBDUploadResult(UploadResult):
39
+ """Includes info on uploaded data """
40
+ pass
41
+
42
+
43
+ class BolExportResults(BaseModel):
44
+ orders: List[Dict[str, Any]]
45
+ errors: List[Dict[str, Any]]
46
+ file_name: str
47
+ order_number_key: str = "OrderNumber"
48
+
49
+ @property
50
+ def is_empty(self) -> bool:
51
+ return len(self.orders) == 0
52
+
53
+
54
+ class PipelineContext(BaseModel):
55
+ """
56
+ PipelineContext is a general storage for any additional data that a step might want to add for other steps to use
57
+ as desired. The primary use is for steps to add entries to the "included" list for process reports.
58
+
59
+ ALL properties in this class may be unset, because only some steps will set them. If you plan to use a property to
60
+ accomplish anything be sure to test that it has been set beforehand.
61
+ """
62
+ # Core pipeline tech.
63
+ previous_output: Any = None # this is set if a step has alt_input and wants to access the original output
64
+ logs: List[str] = [] # Logs captured since the last time this field was cleared.
65
+
66
+ # issue reporting
67
+ issues: List[IssueBase] = []
68
+
69
+ # Process report tech.
70
+ file_config: FileConfig | None = None # Usually set by SFTPFileConfigStep
71
+ included: list[dict[str, str]] = [] # Copied into the end-of-run process report, if reporting is enabled.
72
+ included_files: dict[str, str] = {} # Copied into the end-of-run process report, uploaded like logs to GCS
73
+ snapshot_id: str | None = None # Copied into the end-of-run process report, if reporting is enabled
74
+ indexed_field: str | None = None
75
+ extra_data: Optional[Dict] = {}
76
+ max_sync: MaxSync | None = None
@@ -0,0 +1,20 @@
1
+ from typing import Optional, Union
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from bb_integrations_lib.models.enums import ProbeEventType
6
+ from bb_integrations_lib.models.probe.request_data import RequestData, CreateOrderFromSDOrder, UpdateStatusFromSD, \
7
+ CancelOrderFromSD, SaveBOLsAndDropsFromSD, ErrorRequestData, MacropointLocationUpdate
8
+
9
+
10
+ class ProbeEvent(BaseModel):
11
+ source_probe: str
12
+ type: ProbeEventType
13
+ timestamp: str # Datetime in ISOformat, UTC
14
+ data: Union[CreateOrderFromSDOrder, UpdateStatusFromSD, CancelOrderFromSD, SaveBOLsAndDropsFromSD, ErrorRequestData, MacropointLocationUpdate] = Field(discriminator="request_type")
15
+
16
+ record_id: Optional[str] = Field(description="The DB ID of the relevant record picked up by the probe. Must be set for all Crossroads operations.", default=None)
17
+ record_id_field: Optional[str] = Field(description="The DB field containing the record_id. Must be set for all Crossroads operations.", default=None)
18
+ record_collection: Optional[str] = Field(description="The DB collection this record belongs to. Must be set for all Crossroads operations.", default=None)
19
+
20
+