Created
April 8, 2026 18:01
-
-
Save dodeja/a7171810f578f402322080fe747c13c2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| › I want you to thoroughly review the booking parsers. I'm not really happy with the quality. I want to figure out how we can standardize the implementation details in the interface. Make the code easier to read, dry, and have potential reusability. | |
| ⚠ MCP client for `paper` failed to start: MCP startup failed: handshaking with MCP server failed: Send message error Transport [rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<codex_rmcp_client::rmcp_clie | |
| nt::StreamableHttpResponseClient>>] error: Client error: error sending request for url (http://127.0.0.1:29979/mcp), when send initialize request | |
| ⚠ MCP startup incomplete (failed: notion, paper) | |
| • Running SessionStart hook | |
| SessionStart hook (completed) | |
| • Running UserPromptSubmit hook | |
| UserPromptSubmit hook (completed) | |
| • I'm noticing that the user provided AGENTS instructions mention checking a CLAUDE.md file for an overview of commands. It seems like it could be relevant to what I'm working on, so I should read through that document. Additionally, I think I might need to inspect | |
| the booking parser files, using a tool like rg. I’ll want to start with some commentary before diving into those specific details. Let's see where this leads! | |
| • I’m reviewing the booking parser area first to map the current parser interface, the concrete implementations, and the duplication patterns before I make recommendations. | |
| • Explored | |
| └ Read CLAUDE.md | |
| List . | |
| Search booking|parser | |
| ./apps/tnt-api/app/services/steamship_trackers/maeusynergy/maeusynergy_tracker.rb:126: MaeusynergyBookingParser.new(shipment_data, events: events) | |
| ./apps/tnt-api/app/services/steamship_trackers/maeusynergy/maeusynergy_tracker.rb:128: memo_wise :booking_parser | |
| • I found a dedicated base class plus nine tracker-specific booking parsers. Next I’m reading the base contract, each parser, and the tracker call sites to see where the interface is implicit versus actually standardized. | |
| • Updated Plan | |
| └ Reviewing booking parser implementations and proposing a clearer, reusable contract before recommending refactors. | |
| □ Inspect base booking parser contract and all concrete parser implementations | |
| □ Compare tracker call sites, shared output shapes, and test coverage for interface drift | |
| □ Summarize code review findings and propose a standard parser interface/refactor direction | |
| • Running UserPromptSubmit hook | |
| UserPromptSubmit hook (completed) | |
| › make sure you pull the branch | |
| ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── | |
| • I’m checking the current branch state first so I can pull safely without trampling any local work, then I’ll continue the review on the updated branch. | |
| • Ran git remote -v | |
| └ origin https://github.com/Terminal49/t49.git (fetch) | |
| origin https://github.com/Terminal49/t49.git (push) | |
| • Ran git branch --show-current | |
| └ armanhossain/dev-9197-transport-plan-data-from-booking-responses | |
| • Ran git status --short --branch | |
| └ ## armanhossain/dev-9197-transport-plan-data-from-booking-responses...origin/armanhossain/dev-9197-transport-plan-data-from-booking-responses | |
| ?? .superset/ | |
| • The worktree is clean aside from the untracked .superset/ workspace directory, so a fast-forward pull is safe. I’m pulling the current feature branch from origin now. | |
| • Ran git pull --ff-only origin armanhossain/dev-9197-transport-plan-data-from-booking-responses | |
| └ From https://github.com/Terminal49/t49 | |
| * branch armanhossain/dev-9197-transport-plan-data-from-booking-responses -> FETCH_HEAD | |
| Already up to date. | |
| • I’m continuing to read the files and thinking I might mention how to keep the branch up-to-date. Now, I need to look at the parser files and see how I can group the reads. | |
| I want to list the parser files precisely, which look to be about nine: cmdu, cosuweb, eglv, hdmumobile, maeusynergy, mscudirect, oneyapi, whlctrawler2, ymluapi, and zimu. | |
| • The branch is already current. I’m back on the parser review and reading the base class plus each concrete implementation side by side. | |
| • Explored | |
| └ | |
| ### apps/tnt-api/app/services/steamship_trackers/hdmumobile/*_booking_parser.rb | |
| … +5 lines | |
| ### apps/tnt-api/app/services/steamship_trackers/mscudirect/*_booking_parser.rb | |
| nl: apps/tnt-api/app/services/steamship_trackers/mscudirect/*_booking_parser.rb: No such file or directory | |
| • Ran for f in apps/tnt-api/app/services/steamship_trackers/{cmdu,cosuweb,eglv}/'*_booking_parser.rb'; do printf '\n### %s\n' "$f"; nl -ba "$f" | sed -n '1,260p'; done | |
| └ | |
| • Ran for f in apps/tnt-api/app/services/steamship_trackers/{oneyapi,whlctrawler2,ymluapi,zimu}/'*_booking_parser.rb'; do printf '\n### %s\n' "$f"; nl -ba "$f" | sed -n '1,280p'; done | |
| └ nl: apps/tnt-api/app/services/steamship_trackers/zimu/*_booking_parser.rb: No such file or directory | |
| ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── | |
| • The base class is extremely thin: it standardizes only one output shape and leaves the rest implicit. I’m correcting the file dump and then I’ll compare that against the tracker call sites and specs. | |
| • Explored | |
| └ List steamship_trackers | |
| Search _booking_parser\.rb$ | |
| • Ran for f in $(rg --files apps/tnt-api/app/services/steamship_trackers | rg '_booking_parser\.rb$' | sort | tail -n 5); do printf '\n### %s\n' "$f"; nl -ba "$f" | sed -n '1,280p'; done | |
| └ | |
| ### apps/tnt-api/app/services/steamship_trackers/mscudirect/mscudirect_booking_parser.rb | |
| … +491 lines | |
| 94 end | |
| 95 end | |
| • Ran for f in $(rg --files apps/tnt-api/app/services/steamship_trackers | rg '_booking_parser\.rb$' | sort | head -n 5); do printf '\n### %s\n' "$f"; nl -ba "$f" | sed -n '1,260p'; done | |
| └ | |
| ### apps/tnt-api/app/services/steamship_trackers/cmdu/cmdu_booking_parser.rb | |
| … +438 lines | |
| 179 end | |
| 180 end | |
| • I think I need to check the call sites for the tracker to ensure they’re all using the same methods. It seems important for consistency. I’ll search through the booking_parser methods to find out how they're implemented. By doing this, I'll be able to identify any | |
| discrepancies or issues, and that should help me maintain the integrity and functionality of the code. Let’s get started on that search! | |
| • Ran for f in $(rg -l "booking_parser\.(booked_equipment_groups|booking_transport_plan)" apps/tnt-api/app/services/steamship_trackers -S | sort); do printf '\n### %s\n' "$f"; nl -ba "$f" | sed -n '1,260p' | rg -n "booking_parser|booked_equipment_groups| | |
| │ booking_transport_plan|def booking_parser|memo_wise" -n --context 3; done | |
| └ 133- 133 } | |
| 134- 134 } | |
| … +61 lines | |
| ### apps/tnt-api/app/services/steamship_trackers/zimu/zimu_tracker.rb | |
| • Explored | |
| └ Search booking_parser\.(booked_equipment_groups|booking_transport_plan) in steamship_trackers | |
| Search RSpec.describe (CmduBookingParser|CosuwebBookingParser|EglvBookingParser|HdmumobileBookingParser|MaeusynergyBookingParser|MscudirectBookingParser|OneyapiBookingParser|Whlctrawler2BookingParser|YmluapiBookingParser|ZimuBookingParser)|describe | |
| '#booked_equipment_groups'|describe '#booking_transport_plan' in spec | |
| • I find it interesting that I might only be dealing with the base spec. I should really look into those tracker specs to cover the methods indirectly. To get more details, I'll search for specific tracker names in the specs. I'm especially curious about the tracker | |
| specs related to booking. It sounds like using "rg" could help me dive deeper into this investigation. Let's see where this leads! | |
| ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── | |
| • There’s almost no direct parser-spec coverage. Most of the contract is enforced indirectly through tracker specs, which is part of why the interface has drifted so much. | |
| • Explored | |
| └ Read steamship_booking_parser_spec.rb | |
| Search booking_equipment_groups|booking_transport_plan|pre-container_booking|as_pre-container_booking|booked_equipment_groups in steamship_trackers | |
| • Ran for f in $(rg -l "booking_equipment_groups|booking_transport_plan|booked_equipment_groups" apps/tnt-api/spec/services/steamship_trackers -S | sort); do printf '\n### %s\n' "$f"; nl -ba "$f" | sed -n '1,260p' | rg -n "booking_equipment_groups| | |
| │ booking_transport_plan|booked_equipment_groups|tracks_.*pre-container_booking|tracks_.*as_booking|describe|it " -n --context 2; done | |
| └ | |
| ### apps/tnt-api/spec/services/steamship_trackers/whlctrawler2/whlctrawler2_tracker_spec.rb | |
| … +288 lines | |
| 259: 259 tracking = described_class.new('ZIMUSHH31195062', context: { proxy_service: 'datacenter_brightdata' }) | |
| 260- 260 expect(tracking.tracking_found?).to be_truthy | |
| • I need to look into specific tracker specs and the expected shapes, starting with BookingTransportPlanLeg.build. I might also check other helper classes for good measure. Standardizing the interface could be helpful, so I’ll read up on that as well as | |
| EquipmentIsoLookup, though I'm not sure about it. Additionally, I want to inspect the base tracker and look for the def booked_equipment_groups method in there. Let's see what I can find! | |
| • Explored | |
| └ Read booking_transport_plan_leg.rb | |
| Search def booked_equipment_groups|def booking_transport_plan|BookingTransportPlanLeg\.build|booked_equipment_groups in steamship_trackers | |
| Search class EquipmentIsoLookup|module EquipmentIsoLookup|class EquipmentTypeTextParser|module EquipmentTypeTextParser in services | |
| • I’m thinking that I need to inspect the equipment helpers because it looks like several parsers are duplicating code when it comes to lookup functions. It's probably not ideal since redundancy could lead to more maintenance work later and might cause | |
| inconsistencies. I want to see if I can streamline things or refactor that code to keep it tidy. Maybe there’s a way to create a shared function or something similar! | |
| • Explored | |
| └ Read equipment_type_text_parser.rb, equipment_iso_lookup.rb, eglv_tracker.rb, mscudirect_tracker.rb, cosuweb_tracker.rb, whlctrawler2_tracker.rb, zimu_tracker.rb | |
| • I need to take a closer look at all the parser files for line references to compare patterns. Using a script could help me extract methods or search for duplicate helper patterns. It might be good to check for specific methods like EquipmentIsoLookup.lookup and | |
| BookingTransportPlanLeg.build. I should also note variations in method signatures and any inconsistencies in normalization logic. The user is looking for a general review, but it seems I may need to get more specific with my findings. | |
| ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── | |
| • I’ve confirmed the main drift points: constructor shape, equipment parsing, location normalization, and leg assembly are all reimplemented ad hoc. I’m pulling a few focused cross-file slices now so I can cite the duplication and suggest a cleaner interface instead | |
| of a vague cleanup. | |
| • Explored | |
| └ Search class .*BookingParser|def initialize\(|def booked_equipment_groups|def booking_transport_plan|def raw_equipment_entries in steamship_booking_parser.rb | |
| Search BookingTransportPlanLeg\.build\( in *_booking_parser.rb | |
| Search EquipmentIsoLookup\.lookup|EquipmentTypeTextParser\.parse|Sentry\.capture_message\('Unmapped|def .*transport_plan_location|def .*planned_|def parse_container_type|def parse_.*timestamp|def .*location\( in *_booking_parser.rb | |
| • I need to inspect one or two tracker specs for output consistency, though it may not be necessary. I'm also considering if any parser has a direct constructor override causing an interface issue. The user asks for standardization in implementation details to | |
| enhance code readability and reusability. It sounds more like a review than a directive to modify. Maybe I'll draft a proposed design and offer follow-up implementation or even a prototype refactor. | |
| Considering interface standardization | |
| The feedback suggests there’s some dissatisfaction with quality, which leads me to believe a review of foundational standardization in the interface is needed. The request to "review" indicates I should focus on analysis and recommendations rather than immediate | |
| code changes unless something small is clear. I might prioritize findings first, noting any style inconsistencies and syntax issues, like the formatting in lines 171-172. There’s also an unused parameter in the Ymlu parser that highlights a duplication issue. | |
| Overall, I want to ensure we identify bugs and regressions in this code review. | |
| Identifying parser issues | |
| I need to pinpoint actual issues in the parser implementation beyond just style, as the user is concerned about quality and standardization. My findings should encompass architectural issues and maintainability risks, ordering them by severity. It's not about bugs | |
| necessarily, but highlighting risks. For example, the base class contract is incomplete; it defines raw_equipment_entries and an optional booking_transport_plan but lacks consistency across subclasses. They differ in how they override initialization and interact | |
| with the data shape, leading to a lack of standardization in the interface. | |
| Considering tracker dependencies | |
| I’m thinking about the hidden dependencies on tracker constants within the parsers, like CosuwebTracker::LOCODE_MAP and others. This coupling between the parser and tracker poses a maintainability and reusability issue, which is a valid concern. I want to | |
| highlight this aspect because it could lead to challenges in future updates or adjustments. It’s important to keep these dependencies in check to ensure smoother maintenance down the line. | |
| Standardizing interface implementation | |
| I think using small primitives instead of one giant DSL is a great approach. I can create some pseudocode for a standard interface that includes concise Ruby sketches. A short code sample might be helpful, especially if it adds value. I should consider using | |
| absolute file paths for references, like [steamship_booking_parser.rb](/Users/.../apps/tnt-api/app/services/steamship_booking_parser.rb#L3). I’ll also ensure no edits were made, and I might mark the plan as completed if appropriate. | |
| ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── | |
| • I have enough context now. The main problem isn’t one bad parser; it’s that the base contract is too weak, so every parser redefines the same normalization steps with slightly different rules and failure behavior. | |
| • Updated Plan | |
| └ ✔ Inspect base booking parser contract and all concrete parser implementations | |
| ✔ Compare tracker call sites, shared output shapes, and test coverage for interface drift | |
| □ Summarize code review findings and propose a standard parser interface/refactor direction | |
| • I might not need any additional tools for this. I plan to organize my findings first by severity. It could include open questions and assumptions, followed by proposed standardization. Since the user requested a thorough review, I want to make sure it's detailed | |
| but concise. | |
| I can structure it into three sections: Findings, Standardization Direction, and Rollout. I should highlight that there are no code changes and maybe include a sample interface. I’ll ensure it's clear and includes line references. I'll aim for high signal and keep | |
| it to about five findings, focusing on essential details. | |
| Detailing findings with references | |
| I could include specific references for several files, like steamship_booking_parser, oneyapi_booking_parser, maeusynergy_booking_parser, and eglv_tracker. | |
| For Finding 2, I noticed duplicated equipment resolution with inconsistent dropping and observability. The references for this are found in mscudirect_booking_parser, oney, whlc, eglv, hdmumobile, and ymlu. | |
| Then, for Finding 3, there's duplicated location normalization that's coupled to tracker constants. The relevant line references can be found in cmdu, cosu, hdmumobile, oney, ymlu, zimu, and whlc, along with their dependencies. | |
| I'm considering a code snippet for the SteamshipBookingParser. I might want to include a small part of it in the final output. My recommendations are: first, add shared contract specs and direct parser specs. Then, extract helpers from simpler parsers like Oney, | |
| Ymlu, Mscu, and Cosu. Finally, I suggest tackling the event-driven aspects as a second abstraction. I should mention that no code changes have been made yet; this is just a starting point. Keeping it concise is essential. | |
| • Findings | |
| 1. High: the base class does not actually define a stable parser interface. It standardizes only booked_equipment_groups, leaves booking_transport_plan optional via a default [], and does not define a normalized constructor contract. Concrete parsers already | |
| diverge on initialization and usage, and EglvTracker bypasses the common booking_parser pattern entirely. | |
| apps/tnt-api/app/services/steamship_booking_parser.rb:3 | |
| apps/tnt-api/app/services/steamship_trackers/oneyapi/oneyapi_booking_parser.rb:4 | |
| apps/tnt-api/app/services/steamship_trackers/maeusynergy/maeusynergy_booking_parser.rb:4 | |
| apps/tnt-api/app/services/steamship_trackers/eglv/eglv_tracker.rb:360 | |
| 2. High: equipment parsing is duplicated and has inconsistent failure semantics. Several parsers silently drop unknown equipment types, while others emit Sentry warnings. That means the same class of data loss is observable in some carriers and invisible in | |
| others. | |
| apps/tnt-api/app/services/steamship_trackers/eglv/eglv_booking_parser.rb:20 | |
| apps/tnt-api/app/services/steamship_trackers/hdmumobile/hdmumobile_booking_parser.rb:37 | |
| apps/tnt-api/app/services/steamship_trackers/ymluapi/ymluapi_booking_parser.rb:41 | |
| apps/tnt-api/app/services/steamship_trackers/mscudirect/mscudirect_booking_parser.rb:19 | |
| apps/tnt-api/app/services/steamship_trackers/whlctrawler2/whlctrawler2_booking_parser.rb:54 | |
| apps/tnt-api/app/services/steamship_trackers/zimu/zimu_booking_parser.rb:24 | |
| 3. Medium: location normalization is repeated in almost every parser, with slightly different heuristics for name, city, locode, and raw_location. Some parsers also depend directly on tracker constants, which makes them less reusable and harder to reason about in | |
| isolation. | |
| apps/tnt-api/app/services/steamship_trackers/cmdu/cmdu_booking_parser.rb:74 | |
| apps/tnt-api/app/services/steamship_trackers/cosuweb/cosuweb_booking_parser.rb:34 | |
| apps/tnt-api/app/services/steamship_trackers/oneyapi/oneyapi_booking_parser.rb:48 | |
| apps/tnt-api/app/services/steamship_trackers/ymluapi/ymluapi_booking_parser.rb:80 | |
| apps/tnt-api/app/services/steamship_trackers/zimu/zimu_booking_parser.rb:60 | |
| apps/tnt-api/app/services/steamship_trackers/maeusynergy/maeusynergy_booking_parser.rb:127 | |
| 4. Medium: transport-plan extraction and normalization are mixed together in each parser. Eight parsers build BookingTransportPlanLeg directly, but each one reimplements sequencing, planned-vs-actual selection, vessel/voyage fallback, and location shaping | |
| differently. That makes the code harder to read and nearly impossible to standardize incrementally. | |
| apps/tnt-api/app/services/booking_transport_plan_leg.rb:3 | |
| apps/tnt-api/app/services/steamship_trackers/cmdu/cmdu_booking_parser.rb:20 | |
| apps/tnt-api/app/services/steamship_trackers/maeusynergy/maeusynergy_booking_parser.rb:89 | |
| apps/tnt-api/app/services/steamship_trackers/ymluapi/ymluapi_booking_parser.rb:12 | |
| 5. Medium: test coverage is too indirect for this layer. The only direct parser spec is for the base grouping behavior; concrete parser behavior is mostly validated through tracker specs. That makes refactoring the parser interface riskier than it needs to be. | |
| apps/tnt-api/spec/services/steamship_booking_parser_spec.rb:5 | |
| apps/tnt-api/spec/services/steamship_trackers/oneyapi/oneyapi_tracker_spec.rb:259 | |
| Recommended Interface | |
| I would standardize on one parser contract and keep the current public methods as thin delegates during migration: | |
| class SteamshipBookingParser | |
| Result = Data.define(:equipment_groups, :transport_plan, :warnings) | |
| def result | |
| Result.new( | |
| equipment_groups: normalize_equipment_groups(extract_equipment_rows), | |
| transport_plan: normalize_transport_plan(extract_transport_rows), | |
| warnings: warnings | |
| ) | |
| end | |
| end | |
| Then move shared behavior into the base layer: | |
| - equipment_group_from_iso(iso, units: 1) | |
| - equipment_group_from_dims(length:, height:, type:, source:) | |
| - equipment_group_from_text(text, source:) | |
| - location(name:, city: nil, locode: nil, raw_location: nil) | |
| - leg(...) as the only place that calls BookingTransportPlanLeg.build | |
| - warn_unmapped_code(source:, code:) | |
| That keeps carrier-specific code focused on extraction, not normalization. | |
| Refactor Direction | |
| 1. Add shared parser contract specs first. Every concrete parser should be tested directly for result, booked_equipment_groups, and booking_transport_plan. | |
| 2. Extract equipment helpers next. This is the biggest DRY win and removes the silent-drop inconsistency. | |
| 3. Extract a shared location builder. Most parsers are rebuilding the same four-field hash. | |
| 4. Introduce two small parser styles, not one giant abstraction: | |
| - list-backed route parsers: Cosuweb, ONE, YMLU, ZIMU, HDMU | |
| - paired-event parsers: CMDU, Maeu | |
| 5. Decouple parsers from tracker constants by injecting lookup maps or extracting carrier-specific lookup helpers under the parser namespace. | |
| The main caution is not to force all carriers into one DSL. The reusable layer should be small primitives plus a result object, not a heavy framework. | |
| No code changes yet. If you want, the right first implementation step is: add the shared parser contract and refactor one simple parser family (Oneyapi, Ymluapi, Mscudirect) onto it. | |
| • Running Stop hook | |
| Stop hook (completed) | |
| › Give me examle of shape of three book parsers and how you would rewrite it | |
| • Running UserPromptSubmit hook | |
| UserPromptSubmit hook (completed) | |
| • I need to answer without web access. The user is asking for examples of shapes for three book parsers and how to rewrite them. It seems I should inspect the code and select three representative parsers, like those backed by a simple list, custom equipment only, | |
| and event-paired complex types. | |
| I could craft examples showing the "current shape" and "rewritten shape," including concise code snippets. I also need to clarify that "book parsers" refers to booking parsers and consider referencing relevant files. | |
| Describing current shapes of book parsers | |
| I can outline the current shapes of three representative parsers. For Oneyapi, it involves an array wrapping and parsing equipment from containerTypeSize and the route from voyageList. Ymluapi might have a special fallback. Maeusynergy deals with event-pairing, | |
| showcasing varying complexity levels. | |
| I'll present a summary of the current shapes and the rewriting using standardized base helpers. This will include code snippets in Ruby while keeping it concise with examples for each parser. I'll illustrate potential issues with the OneyapiBookingParser, | |
| including how to address those in the rewrite. | |
| Organizing file references | |
| I’m thinking it might be clearer to place each file reference on its own line when I write them out. Maybe I can use bullet points for better readability? That could really help with organization. I’ll start with a section titled Proposed Base and then list out | |
| three examples underneath. This way, everything feels structured and easy to follow. Let's get that organized! | |
| • The cleanest way to standardize these is to make each parser responsible only for extraction, and push normalization into the base class. | |
| Current examples: | |
| apps/tnt-api/app/services/steamship_trackers/oneyapi/oneyapi_booking_parser.rb:3 | |
| apps/tnt-api/app/services/steamship_trackers/ymluapi/ymluapi_booking_parser.rb:3 | |
| apps/tnt-api/app/services/steamship_trackers/cmdu/cmdu_booking_parser.rb:3 | |
| Proposed Base Shape | |
| class SteamshipBookingParser | |
| def booked_equipment_groups | |
| extract_equipment_groups | |
| .compact | |
| .select { |row| row[:units].to_i.positive? } | |
| .group_by { |row| row[:container_iso] } | |
| .map { |iso, rows| { container_iso: iso, units: rows.sum { _1[:units].to_i } } } | |
| end | |
| def booking_transport_plan | |
| extract_transport_plan.filter_map { |attrs| leg(**attrs) } | |
| end | |
| private | |
| def extract_equipment_groups | |
| [] | |
| end | |
| def extract_transport_plan | |
| [] | |
| end | |
| def equipment_from_text(text, units: 1, source:) | |
| dims = EquipmentTypeTextParser.parse(text) | |
| return warn_unmapped(source, text) unless dims | |
| equipment_from_dims(**dims, units: units, source: source) | |
| end | |
| def equipment_from_dims(length:, height: :standard, type:, units: 1, source:) | |
| iso = EquipmentIsoLookup.lookup(length: length, height: height, type: type) | |
| return warn_unmapped(source, { length:, height:, type: }) unless iso | |
| { container_iso: iso, units: units } | |
| end | |
| def equipment_from_iso(iso, units: 1) | |
| return if iso.blank? | |
| { container_iso: iso, units: units } | |
| end | |
| def location(name:, locode: nil, city: nil, raw_location: nil) | |
| return if name.blank? && locode.blank? | |
| { | |
| name: name || city || locode, | |
| city: city || name, | |
| locode: locode, | |
| raw_location: raw_location || name | |
| }.compact | |
| end | |
| def leg(**attrs) | |
| BookingTransportPlanLeg.build(**attrs) | |
| end | |
| def warn_unmapped(source, value) | |
| Sentry.capture_message("Unmapped booking parser value", level: :warning, extra: { source:, value: }) | |
| nil | |
| end | |
| end | |
| That gives every parser the same shape: extract_equipment_groups and extract_transport_plan. | |
| 1. Oneyapi: Simple List Parser | |
| Current shape: | |
| - wraps data in initialize | |
| - parses equipment inline | |
| - builds locations inline | |
| - planned/actual timestamp logic inline | |
| Rewrite: | |
| class OneyapiBookingParser < SteamshipBookingParser | |
| def initialize(data) | |
| @entries = Array.wrap(data) | |
| end | |
| private | |
| def extract_equipment_groups | |
| @entries.filter_map do |entry| | |
| equipment_from_text( | |
| normalize_container_type(entry["containerTypeSize"]), | |
| source: "oneyapi.containerTypeSize" | |
| ) | |
| end | |
| end | |
| def extract_transport_plan | |
| voyage_list.map.with_index(1) do |voyage, sequence| | |
| { | |
| load_location: location_from_stop(voyage["pol"]), | |
| discharge_location: location_from_stop(voyage["pod"]), | |
| sequence: sequence, | |
| vessel_name: voyage["vesselEngName"], | |
| voyage_number: [voyage["scheduleVoyageNumber"], voyage["scheduleDirectionCode"]].compact.join, | |
| mode: "vessel", | |
| etd_at: planned_departure(voyage), | |
| eta_at: planned_arrival(voyage), | |
| atd_at: voyage.dig("pol", "isActual") ? voyage.dig("pol", "date") : nil, | |
| ata_at: voyage.dig("pod", "isArrivalActual") ? voyage.dig("pod", "arrivalDate") : nil | |
| } | |
| end | |
| end | |
| def voyage_list | |
| Array(@entries.first&.dig("voyageList")) | |
| end | |
| def normalize_container_type(value) | |
| value.to_s.tr("'", " ").gsub(/\.\s*$/, "").squish | |
| end | |
| def location_from_stop(stop) | |
| return if stop.blank? | |
| location( | |
| name: stop["locationName"]&.split(",")&.first&.strip, | |
| city: stop["locationName"]&.split(",")&.first&.strip, | |
| locode: stop["locationCode"], | |
| raw_location: stop["locationName"] | |
| ) | |
| end | |
| def planned_departure(voyage) | |
| voyage.dig("pol", "isActual") ? nil : voyage.dig("pol", "date") | |
| end | |
| def planned_arrival(voyage) | |
| return if voyage.dig("pod", "isArrivalActual") | |
| voyage.dig("pod", "arrivalDate") || voyage.dig("pod", "date") | |
| end | |
| end | |
| This is still carrier-specific, but the parser is now just extraction. | |
| 2. Ymluapi: Normalize Payload Once | |
| Current shape issues: | |
| - repeated @data.is_a?(Hash) && @data.key?... | |
| - custom summary token parsing mixed with transport logic | |
| - location lookup and root selection are entangled | |
| Rewrite: | |
| class YmluapiBookingParser < SteamshipBookingParser | |
| def initialize(data) | |
| root = data.is_a?(Hash) && data.key?(:bl_data) ? data[:bl_data] : data | |
| @basic_info = root["basicInfo"] || {} | |
| @routing_schedule = root.dig("routingInfo", "routingSchedule") || [] | |
| end | |
| private | |
| def extract_equipment_groups | |
| summary_tokens.filter_map do |qty, type_text| | |
| equipment_from_text(type_text, units: qty, source: "ymluapi.ctnrUnit") | |
| end | |
| end | |
| def extract_transport_plan | |
| @routing_schedule.each_cons(2).with_index(1).map do |(from_stop, to_stop), sequence| | |
| { | |
| load_location: schedule_location(from_stop), | |
| discharge_location: schedule_location(to_stop), | |
| sequence: sequence, | |
| vessel_name: @basic_info["vesselName"], | |
| voyage_number: @basic_info["voyageCode"], | |
| mode: "vessel", | |
| etd_at: planned_timestamp(from_stop), | |
| eta_at: planned_timestamp(to_stop), | |
| atd_at: actual_timestamp(from_stop), | |
| ata_at: actual_timestamp(to_stop) | |
| } | |
| end | |
| end | |
| def summary_tokens | |
| inner = @basic_info["ctnrUnit"].to_s[/\((.+)\)/, 1] | |
| return [] if inner.blank? | |
| inner.split(",").filter_map do |token| | |
| token = token.strip | |
| next if token.blank? | |
| if (match = token.match(/\A(?<qty>\d+)\s*[x*]\s*(?<type>.+)\z/i)) | |
| [match[:qty].to_i, match[:type].strip] | |
| elsif (match = token.match(/\A(?<type>[^*]+)\*(?<qty>\d+)\z/)) | |
| [match[:qty].to_i, match[:type].strip] | |
| else | |
| [1, token] | |
| end | |
| end | |
| end | |
| def schedule_location(stop) | |
| name = stop["placeName"] | |
| location( | |
| name: name, | |
| city: name, | |
| locode: extract_locode(name), | |
| raw_location: name | |
| ) | |
| end | |
| def extract_locode(value) | |
| value.to_s[/\(([A-Z]{5})\)/, 1] | |
| end | |
| def planned_timestamp(stop) | |
| stop["dateQlfr"] == "Actual" ? nil : stop["dateTime"] | |
| end | |
| def actual_timestamp(stop) | |
| stop["dateQlfr"] == "Estimated" ? nil : stop["dateTime"] | |
| end | |
| end | |
| The biggest gain here is payload normalization in initialize. The rest becomes straightforward. | |
| 3. Cmdu: Move DCSA Logic Into a Shared Parser Family | |
| CmduBookingParser is really a DCSA parser, not just a CMDU parser. That pairing logic should not live in the carrier class. | |
| Rewrite as a reusable base: | |
| class DcsaBookingParser < SteamshipBookingParser | |
| def initialize(events) | |
| @events = Array(events) | |
| end | |
| private | |
| def extract_equipment_groups | |
| @events | |
| .select { _1["eventType"] == "EQUIPMENT" } | |
| .filter_map { |event| equipment_from_iso(event["ISOEquipmentCode"]) } | |
| end | |
| def extract_transport_plan | |
| paired_transport_events.map.with_index(1) do |(departure, arrival), sequence| | |
| { | |
| load_location: dcsa_location(departure), |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment