<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jessica Williams</title>
    <description>The latest articles on DEV Community by Jessica Williams (@williamsj04).</description>
    <link>https://dev.to/williamsj04</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2847209%2F337b0c17-9078-4474-ab0f-b17dc01432cb.png</url>
      <title>DEV Community: Jessica Williams</title>
      <link>https://dev.to/williamsj04</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/williamsj04"/>
    <language>en</language>
    <item>
      <title>Driver Hours Tracking Without Double Counting: How a Fleet Backend Should Merge Ignition, GPS and Manual Logs</title>
      <dc:creator>Jessica Williams</dc:creator>
      <pubDate>Thu, 12 Nov 2026 12:28:17 +0000</pubDate>
      <link>https://dev.to/williamsj04/driver-hours-tracking-without-double-counting-how-a-fleet-backend-should-merge-ignition-gps-and-553c</link>
      <guid>https://dev.to/williamsj04/driver-hours-tracking-without-double-counting-how-a-fleet-backend-should-merge-ignition-gps-and-553c</guid>
      <description>&lt;p&gt;A fleet vehicle can generate several versions of the same working period. The ignition system records when the engine starts, GPS tracking identifies movement, and drivers may manually enter duty or trip details. If a backend treats every record as a separate activity, the result is inflated driving time, incorrect utilisation reports, and unreliable driver-hour calculations.&lt;/p&gt;

&lt;p&gt;The challenge is not collecting more data. It is deciding which events describe the same activity, which source should be trusted, and how corrections should affect previously calculated totals.&lt;/p&gt;

&lt;p&gt;A reliable fleet management backend needs a unified event model that can reconcile ignition, GPS, and manual logs without counting the same period twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Driver Hours Become Inaccurate
&lt;/h2&gt;

&lt;p&gt;Consider a vehicle that starts at 8:00 AM, begins moving at 8:07 AM, stops at 9:15 AM, and switches off at 9:20 AM.&lt;/p&gt;

&lt;p&gt;Three systems may produce different records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ignition: Engine on at 8:00 AM and off at 9:20 AM.&lt;/li&gt;
&lt;li&gt;GPS: Movement detected from 8:07 AM to 9:15 AM.&lt;/li&gt;
&lt;li&gt;Driver app: Trip manually recorded from 8:05 AM to 9:18 AM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not necessarily three separate activities. They are three observations of one operational period.&lt;/p&gt;

&lt;p&gt;A poorly designed backend might add all three durations together. A slightly better system might select one source and ignore the others, losing useful information.&lt;/p&gt;

&lt;p&gt;The right approach is to preserve each source event while calculating a single reconciled activity timeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Store Raw Events Before Calculating Driver Hours
&lt;/h2&gt;

&lt;p&gt;Do not directly update a driver's total hours whenever a new GPS or ignition event arrives.&lt;/p&gt;

&lt;p&gt;Instead, create an immutable event record that captures the original information.&lt;/p&gt;

&lt;p&gt;A practical event structure could contain:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;event_id&lt;/td&gt;
&lt;td&gt;Unique event identifier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vehicle_id&lt;/td&gt;
&lt;td&gt;Vehicle associated with the event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;driver_id&lt;/td&gt;
&lt;td&gt;Assigned or identified driver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;source&lt;/td&gt;
&lt;td&gt;Ignition, GPS, or manual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;event_type&lt;/td&gt;
&lt;td&gt;Start, stop, movement, correction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;event_time&lt;/td&gt;
&lt;td&gt;Time reported by the source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;received_at&lt;/td&gt;
&lt;td&gt;Time received by the backend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;timezone&lt;/td&gt;
&lt;td&gt;Original timezone or offset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;metadata&lt;/td&gt;
&lt;td&gt;Source-specific information&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The distinction between event_time and received_at is important.&lt;/p&gt;

&lt;p&gt;A telematics device may send an event several minutes late because of poor connectivity. If the backend uses only the time it received the event, the calculated trip timeline may be wrong.&lt;/p&gt;

&lt;p&gt;Store both timestamps and use the event's actual occurrence time for timeline reconstruction.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Assign Source Reliability by Event Type
&lt;/h2&gt;

&lt;p&gt;Not every data source is equally reliable for every measurement.&lt;/p&gt;

&lt;p&gt;Ignition data is useful for determining when an engine was switched on or off. GPS is useful for identifying movement and location changes. Manual logs can provide driver context, corrections, and operational details that devices cannot capture.&lt;/p&gt;

&lt;p&gt;A fleet backend should therefore use event-specific source priorities rather than one universal source hierarchy.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;Primary evidence&lt;/th&gt;
&lt;th&gt;Supporting evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Engine state&lt;/td&gt;
&lt;td&gt;Ignition signal&lt;/td&gt;
&lt;td&gt;Telematics status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vehicle movement&lt;/td&gt;
&lt;td&gt;GPS and speed data&lt;/td&gt;
&lt;td&gt;Odometer changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Driver identity&lt;/td&gt;
&lt;td&gt;Authenticated driver session&lt;/td&gt;
&lt;td&gt;Assignment records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duty declaration&lt;/td&gt;
&lt;td&gt;Driver-entered record&lt;/td&gt;
&lt;td&gt;Fleet supervisor approval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trip boundaries&lt;/td&gt;
&lt;td&gt;Combined event timeline&lt;/td&gt;
&lt;td&gt;Manual correction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This does not mean a GPS record should always override a manual entry. A driver may have a legitimate reason to correct a missing or incorrectly recorded event.&lt;/p&gt;

&lt;p&gt;The system should preserve the original observation, the correction, and the person or process responsible for approving it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Build a Unified Activity Timeline
&lt;/h2&gt;

&lt;p&gt;Once raw events are stored, the backend can group related events into operational sessions.&lt;/p&gt;

&lt;p&gt;A session might begin when ignition is detected or when a valid manual trip start is submitted. GPS movement can confirm that the vehicle has started travelling.&lt;/p&gt;

&lt;p&gt;The backend should use a configurable time window to associate events that are likely to belong to the same activity.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;08:00 - Ignition ON
08:05 - Manual trip started
08:07 - GPS movement detected
09:15 - GPS movement stopped
09:20 - Ignition OFF
09:22 - Manual trip completed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reconciled activity should be one session, not three separate sessions.&lt;/p&gt;

&lt;p&gt;A simplified session record could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"vehicle_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"VH-204"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"driver_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"DR-118"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"session_start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"08:00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"session_end"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"09:20:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"movement_start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"08:07:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"movement_end"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"09:15:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"sources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"ignition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"gps"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"manual"&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"reconciliation_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"matched"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important distinction is between engine-on duration, movement duration, and declared working duration. They should remain separate measurements.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Prevent Duplicate Events with Idempotency
&lt;/h2&gt;

&lt;p&gt;Telematics devices frequently retry requests when they do not receive an acknowledgement. Mobile applications can also resend events after reconnecting.&lt;/p&gt;

&lt;p&gt;Without duplicate protection, the same ignition or GPS event may be processed more than once.&lt;/p&gt;

&lt;p&gt;Use an idempotency strategy based on a stable event identifier.&lt;/p&gt;

&lt;p&gt;If the source provides a unique event ID, use it. Otherwise, generate a deterministic key from suitable attributes such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vehicle identifier&lt;/li&gt;
&lt;li&gt;Source identifier&lt;/li&gt;
&lt;li&gt;Event type&lt;/li&gt;
&lt;li&gt;Event timestamp&lt;/li&gt;
&lt;li&gt;Device sequence number&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A database constraint can prevent duplicate insertion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;unique_vehicle_source_event&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;fleet_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;vehicle_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;source_event_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For systems where the source does not provide a stable ID, use a carefully designed deduplication key. Avoid relying only on timestamps because two legitimate events may occur within the same second.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Handle Late and Out-of-Order Events
&lt;/h2&gt;

&lt;p&gt;A GPS device may upload stored data after returning to network coverage. A manual correction may also arrive after a supervisor reviews the trip.&lt;/p&gt;

&lt;p&gt;The backend should not assume that events arrive in chronological order.&lt;/p&gt;

&lt;p&gt;A better processing model includes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Save the incoming event.&lt;/li&gt;
&lt;li&gt;Validate its source, vehicle, and timestamp.&lt;/li&gt;
&lt;li&gt;Identify the affected activity window.&lt;/li&gt;
&lt;li&gt;Recalculate that window.&lt;/li&gt;
&lt;li&gt;Update derived session records.&lt;/li&gt;
&lt;li&gt;Preserve the previous calculation for audit purposes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach is safer than permanently adding each event to a driver's daily total.&lt;/p&gt;

&lt;p&gt;For example, if an ignition-off event arrives after a GPS trip has already been closed, the backend can revise the engine-on duration without creating a second trip.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Keep Corrections Separate from Original Events
&lt;/h2&gt;

&lt;p&gt;Manual logs are particularly important when a device stops reporting.&lt;/p&gt;

&lt;p&gt;However, allowing drivers or supervisors to overwrite raw telematics records creates audit problems.&lt;/p&gt;

&lt;p&gt;Use a correction table or adjustment record instead.&lt;/p&gt;

&lt;p&gt;A correction should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Original event or session reference&lt;/li&gt;
&lt;li&gt;Corrected value&lt;/li&gt;
&lt;li&gt;Reason for the correction&lt;/li&gt;
&lt;li&gt;Submitted by&lt;/li&gt;
&lt;li&gt;Approved by, where required&lt;/li&gt;
&lt;li&gt;Submission and approval timestamps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system can then show both the original device record and the approved correction.&lt;/p&gt;

&lt;p&gt;This makes driver-hour calculations easier to investigate when payroll, operations, or compliance teams question a reported total.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Separate Driving Time from Engine and Duty Time
&lt;/h2&gt;

&lt;p&gt;A fleet application should not treat engine-on time as equivalent to driving time.&lt;/p&gt;

&lt;p&gt;A vehicle may remain switched on while parked. A driver may also perform loading, inspection, waiting, or other duties while the vehicle is stationary.&lt;/p&gt;

&lt;p&gt;A useful reporting model separates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Engine-on duration&lt;/li&gt;
&lt;li&gt;Vehicle movement duration&lt;/li&gt;
&lt;li&gt;Declared duty duration&lt;/li&gt;
&lt;li&gt;Idle duration&lt;/li&gt;
&lt;li&gt;Rest or off-duty periods, where applicable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Regulatory driving and working-time definitions vary by jurisdiction. A custom fleet application should apply the relevant rules for the operating region rather than assuming that a single calculation works everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Monitor Reconciliation Quality
&lt;/h2&gt;

&lt;p&gt;A fleet manager should be able to see whether the system is reconciling records successfully.&lt;/p&gt;

&lt;p&gt;Useful monitoring metrics include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Percentage of GPS events matched to ignition sessions&lt;/li&gt;
&lt;li&gt;Number of duplicate events rejected&lt;/li&gt;
&lt;li&gt;Late-event frequency&lt;/li&gt;
&lt;li&gt;Manual correction frequency&lt;/li&gt;
&lt;li&gt;Unmatched trip records&lt;/li&gt;
&lt;li&gt;Sessions with conflicting driver assignments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These indicators help identify device failures, driver-app issues, and weaknesses in the data pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Custom Fleet Software Development Helps
&lt;/h2&gt;

&lt;p&gt;A standard telematics dashboard may display vehicle activity, but businesses with multiple device providers, driver applications, and operational systems often need more control over event reconciliation.&lt;/p&gt;

&lt;p&gt;Custom fleet software development can bring these sources into one backend, apply consistent business rules, and expose reconciled data to dispatch, payroll, maintenance, and reporting systems.&lt;/p&gt;

&lt;p&gt;Dev Technosys can be considered among the companies for &lt;a href="https://devtechnosys.com/fleet-management-app-development.php" rel="noopener noreferrer"&gt;fleet management app development&lt;/a&gt;, telematics integrations, driver-facing mobile apps, and backend workflows. Its development approach can be relevant when a fleet operator needs application logic designed around its existing devices and operating processes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Accurate driver-hour tracking depends on more than collecting ignition and GPS signals. It requires a backend that understands how different observations relate to the same operational activity.&lt;/p&gt;

&lt;p&gt;By preserving raw events, applying source-specific rules, preventing duplicate processing, and maintaining an auditable correction history, fleet operators can build more dependable driver-hour reports.&lt;/p&gt;

&lt;p&gt;The objective is simple: one operational activity should produce one reconciled record, even when several systems report it.&lt;/p&gt;

</description>
      <category>fleetmanagement</category>
      <category>telematics</category>
      <category>backend</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Inter-Warehouse Stock Transfers Without Phantom Inventory: A Ledger Design for Supply Chain Software</title>
      <dc:creator>Jessica Williams</dc:creator>
      <pubDate>Wed, 11 Nov 2026 08:01:03 +0000</pubDate>
      <link>https://dev.to/williamsj04/inter-warehouse-stock-transfers-without-phantom-inventory-a-ledger-design-for-supply-chain-software-18k3</link>
      <guid>https://dev.to/williamsj04/inter-warehouse-stock-transfers-without-phantom-inventory-a-ledger-design-for-supply-chain-software-18k3</guid>
      <description>&lt;p&gt;A warehouse transfer can look complete in the system while the physical goods are still on a truck, waiting at a loading dock or delayed during receiving. When the source warehouse reduces its stock before the destination confirms receipt, inventory visibility becomes unreliable.&lt;/p&gt;

&lt;p&gt;This creates a familiar problem: phantom inventory. The system reports stock at a location where it has not yet arrived, or it shows stock as unavailable even though it is already moving through the network.&lt;/p&gt;

&lt;p&gt;The solution is not simply to update two warehouse quantities faster. It is to model transfers as traceable inventory movements with explicit states, quantities and timestamps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Warehouse Transfers Create Inventory Discrepancies
&lt;/h2&gt;

&lt;p&gt;Consider a distributor moving 100 units from Warehouse A to Warehouse B.&lt;/p&gt;

&lt;p&gt;If the system immediately subtracts 100 units from Warehouse A and adds them to Warehouse B, the inventory report may show all 100 units as available at the destination before the shipment arrives.&lt;/p&gt;

&lt;p&gt;If the shipment contains only 96 units, the discrepancy becomes harder to investigate. The system has already represented the transfer as complete, even though four units are missing.&lt;/p&gt;

&lt;p&gt;Common causes include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Delayed shipment confirmation&lt;/li&gt;
&lt;li&gt;Partial receipts&lt;/li&gt;
&lt;li&gt;Duplicate transfer messages&lt;/li&gt;
&lt;li&gt;Incorrect SKU or unit-of-measure mapping&lt;/li&gt;
&lt;li&gt;Failed warehouse integrations&lt;/li&gt;
&lt;li&gt;Manual stock adjustments&lt;/li&gt;
&lt;li&gt;Receiving transactions posted before inspection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A dependable supply chain application should represent the difference between stock at the source, stock in transit and stock physically received at the destination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design the Transfer as a State Machine
&lt;/h2&gt;

&lt;p&gt;A transfer should move through controlled business states instead of changing warehouse balances in one unqualified operation.&lt;/p&gt;

&lt;p&gt;A practical lifecycle is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Draft: The transfer has been created but not approved.&lt;/li&gt;
&lt;li&gt;Approved: The source warehouse can prepare the goods.&lt;/li&gt;
&lt;li&gt;Picked: The goods have been picked and verified.&lt;/li&gt;
&lt;li&gt;Dispatched: The shipment has left the source warehouse.&lt;/li&gt;
&lt;li&gt;In transit: The dispatched quantity is tracked between locations.&lt;/li&gt;
&lt;li&gt;Partially received: The destination has received only part of the shipment.&lt;/li&gt;
&lt;li&gt;Received: The accepted quantity has been confirmed.&lt;/li&gt;
&lt;li&gt;Exception or closed: Any shortage, damage or unresolved quantity is handled according to policy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Not every operation needs a separate state, but dispatch and receipt should never be treated as the same event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use an Inventory Movement Ledger
&lt;/h2&gt;

&lt;p&gt;Instead of storing only the latest stock quantity, record each inventory movement as an immutable business event.&lt;/p&gt;

&lt;p&gt;A ledger entry can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Movement ID&lt;/li&gt;
&lt;li&gt;Transfer ID&lt;/li&gt;
&lt;li&gt;SKU or product variant&lt;/li&gt;
&lt;li&gt;Source and destination warehouse&lt;/li&gt;
&lt;li&gt;Quantity and unit of measure&lt;/li&gt;
&lt;li&gt;Movement type&lt;/li&gt;
&lt;li&gt;Transaction status&lt;/li&gt;
&lt;li&gt;Event timestamp&lt;/li&gt;
&lt;li&gt;User or service responsible&lt;/li&gt;
&lt;li&gt;Reference to the original document&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simplified transfer ledger might look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Quantity&lt;/th&gt;
&lt;th&gt;Inventory effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Picked at Warehouse A&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;Allocated for transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dispatched from A&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;Source stock decreases; in-transit stock increases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Received at B&lt;/td&gt;
&lt;td&gt;96&lt;/td&gt;
&lt;td&gt;In-transit stock decreases; accepted stock at B increases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shortage investigation&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Remaining quantity stays unresolved until disposition&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The exact accounting treatment depends on the inventory model, but the essential rule is consistent: each transition must have a traceable cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate On-Hand, Reserved and Available Inventory
&lt;/h2&gt;

&lt;p&gt;One stock number is not enough for a multi-warehouse operation.&lt;/p&gt;

&lt;p&gt;A useful inventory model distinguishes between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On-hand: Stock physically recorded at a warehouse.&lt;/li&gt;
&lt;li&gt;Reserved: Stock committed to an order or process.&lt;/li&gt;
&lt;li&gt;Available: Stock that can be allocated to new demand.&lt;/li&gt;
&lt;li&gt;In transit: Stock dispatched but not yet received.&lt;/li&gt;
&lt;li&gt;Quarantined: Stock awaiting inspection or disposition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This distinction prevents stock in transit from being incorrectly presented as immediately available at the destination.&lt;/p&gt;

&lt;p&gt;It also helps planners avoid allocating the same units to several orders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevent Duplicate Transfer Events
&lt;/h2&gt;

&lt;p&gt;Warehouse systems often communicate through APIs, message queues or scheduled integration jobs. Network retries can cause the same dispatch or receipt event to arrive more than once.&lt;/p&gt;

&lt;p&gt;The backend should therefore use unique event identifiers and idempotent processing.&lt;/p&gt;

&lt;p&gt;If the destination sends the same receipt confirmation twice, the system should recognise the duplicate and avoid adding the quantity twice.&lt;/p&gt;

&lt;p&gt;A reliable integration should also support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event sequence validation&lt;/li&gt;
&lt;li&gt;Retry handling&lt;/li&gt;
&lt;li&gt;Failed-message queues&lt;/li&gt;
&lt;li&gt;Reconciliation reports&lt;/li&gt;
&lt;li&gt;Alerts for delayed confirmations&lt;/li&gt;
&lt;li&gt;Controlled correction workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not silently discard events that arrive out of order. Store the event, determine whether it can be applied safely and flag unresolved dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle Partial Receipts and Exceptions
&lt;/h2&gt;

&lt;p&gt;A destination warehouse may receive fewer units than were dispatched. Some items may be damaged, incorrectly labelled or missing.&lt;/p&gt;

&lt;p&gt;The receiving workflow should capture the actual quantities separately from the dispatched quantities.&lt;/p&gt;

&lt;p&gt;For example, if 100 units are shipped and 96 are accepted, the system should not automatically mark the remaining four as received. It should preserve the discrepancy and route it for investigation.&lt;/p&gt;

&lt;p&gt;Depending on the business process, the remaining quantity may be recorded as short-shipped, damaged, lost, pending inspection or awaiting a subsequent delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconciliation Across WMS and ERP Systems
&lt;/h2&gt;

&lt;p&gt;Many organisations use a warehouse management system (WMS) for operational stock movements and an enterprise resource planning (ERP) system for broader inventory and financial records.&lt;/p&gt;

&lt;p&gt;The integration must define which system owns each data element and transaction. If both systems independently update the same balance, timing differences can create false discrepancies.&lt;/p&gt;

&lt;p&gt;A better approach is to establish clear ownership, use unique transaction references and reconcile outstanding movements regularly. Microsoft’s guidance on external WMS integration also highlights the importance of understanding transaction timing and avoiding automatic adjustments that conceal the cause of a mismatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Supply Chain Software Development Companies
&lt;/h2&gt;

&lt;p&gt;Businesses planning ledger-based inventory and warehouse integrations should evaluate partners on transaction design, integration reliability and exception handling.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Dev Technosys
&lt;/h3&gt;

&lt;p&gt;Dev Technosys develops custom software and applications that can be designed around warehouse operations, inventory visibility, APIs and business-specific workflows. It is a relevant option for organisations planning supply chain software development with inter-warehouse transfers, audit trails and multi-location inventory controls.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Oracle
&lt;/h3&gt;

&lt;p&gt;Oracle provides enterprise supply chain and inventory management technology. Oracle suits organisations evaluating connected inventory processes and enterprise-wide operational visibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Microsoft
&lt;/h3&gt;

&lt;p&gt;Microsoft offers supply chain capabilities through Dynamics 365, including inventory and warehouse-related workflows. Microsoft suits businesses seeking to connect operational inventory processes with broader ERP functionality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Inter-warehouse transfers should be modelled as a sequence of verifiable movements rather than two unrelated quantity updates.&lt;/p&gt;

&lt;p&gt;A ledger-based design makes stock in transit visible, preserves partial receipts and gives operations teams a clear way to investigate shortages. For any &lt;a href="https://devtechnosys.com/supply-chain-management-software-development.php" rel="noopener noreferrer"&gt;supply chain software development company&lt;/a&gt; building multi-warehouse systems, these controls are essential to reliable inventory visibility.&lt;/p&gt;

</description>
      <category>supplychain</category>
      <category>softwaredevelopment</category>
      <category>inventory</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Cash Drawer Reconciliation in POS Software: Handling Voids, Refunds and Shift Close Without Drift</title>
      <dc:creator>Jessica Williams</dc:creator>
      <pubDate>Fri, 06 Nov 2026 07:55:03 +0000</pubDate>
      <link>https://dev.to/williamsj04/cash-drawer-reconciliation-in-pos-software-handling-voids-refunds-and-shift-close-without-drift-26cf</link>
      <guid>https://dev.to/williamsj04/cash-drawer-reconciliation-in-pos-software-handling-voids-refunds-and-shift-close-without-drift-26cf</guid>
      <description>&lt;p&gt;A cash drawer can appear balanced during a busy shift and still produce a discrepancy when the cashier closes it. A refund may have been recorded against the wrong payment method, a void may have been processed after payment capture, or a cash transaction may have been entered incorrectly.&lt;/p&gt;

&lt;p&gt;These problems are not always caused by cashier mistakes. Sometimes, the underlying POS workflow does not maintain a consistent relationship between orders, payments, cash movements, refunds and shift records.&lt;/p&gt;

&lt;p&gt;Reliable cash drawer reconciliation requires more than comparing the expected cash amount with the physical drawer balance. It requires a transaction history that explains every movement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Cash Drawer Reconciliation Becomes Difficult
&lt;/h2&gt;

&lt;p&gt;A POS system records several types of financial events during a shift:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cash sales&lt;/li&gt;
&lt;li&gt;Card and digital payments&lt;/li&gt;
&lt;li&gt;Partial payments&lt;/li&gt;
&lt;li&gt;Refunds&lt;/li&gt;
&lt;li&gt;Voided transactions&lt;/li&gt;
&lt;li&gt;Cash paid in or paid out&lt;/li&gt;
&lt;li&gt;Drawer openings&lt;/li&gt;
&lt;li&gt;Shift transfers and closures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If these events are stored separately without a consistent transaction reference, the closing balance can become difficult to verify.&lt;/p&gt;

&lt;p&gt;For example, a customer purchases products worth ₹2,000 and pays in cash. Later, the cashier refunds one product worth ₹500. If the refund is recorded as a generic adjustment instead of a linked cash refund, the system may show an incorrect expected drawer balance.&lt;/p&gt;

&lt;p&gt;The reconciliation process should therefore connect each financial event to its original transaction wherever applicable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a Clear Cash Movement Ledger
&lt;/h2&gt;

&lt;p&gt;A dependable POS reconciliation workflow should maintain a ledger of cash movements rather than relying only on the current drawer total.&lt;/p&gt;

&lt;p&gt;Each movement should record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unique movement ID&lt;/li&gt;
&lt;li&gt;Store and terminal ID&lt;/li&gt;
&lt;li&gt;Cashier or employee ID&lt;/li&gt;
&lt;li&gt;Shift ID&lt;/li&gt;
&lt;li&gt;Related order or payment ID&lt;/li&gt;
&lt;li&gt;Movement type&lt;/li&gt;
&lt;li&gt;Amount&lt;/li&gt;
&lt;li&gt;Timestamp&lt;/li&gt;
&lt;li&gt;Reason or adjustment note&lt;/li&gt;
&lt;li&gt;Approval details, where required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Movement types should distinguish sales, refunds, void-related reversals, cash additions, payouts and corrections.&lt;/p&gt;

&lt;p&gt;This structure helps managers trace the exact reason behind a drawer difference instead of treating every discrepancy as an unexplained shortage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Voids Without Distorting the Drawer
&lt;/h2&gt;

&lt;p&gt;A void and a refund are different financial events.&lt;/p&gt;

&lt;p&gt;A void generally cancels a transaction or payment before it is fully settled, depending on the payment provider and transaction stage. A refund returns money after a payment has been completed.&lt;/p&gt;

&lt;p&gt;A POS system should not treat both actions as identical negative entries.&lt;/p&gt;

&lt;p&gt;For example, if a cashier accidentally adds an item before payment, the item may be voided without any cash movement. If the customer has already paid, removing the item may require a refund.&lt;/p&gt;

&lt;p&gt;The software should preserve the original transaction and record the reversal as a separate event. This prevents the system from silently rewriting financial history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refunds Must Follow the Original Payment Method
&lt;/h2&gt;

&lt;p&gt;Refund handling becomes complicated when an order uses multiple payment methods.&lt;/p&gt;

&lt;p&gt;Consider an order paid through ₹1,000 cash and ₹1,000 card. If the customer returns a ₹500 item, the POS should follow the store's refund policy and payment-provider capabilities.&lt;/p&gt;

&lt;p&gt;The system should record:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The original payment allocation.&lt;/li&gt;
&lt;li&gt;The returned item and approved refund amount.&lt;/li&gt;
&lt;li&gt;The refund method.&lt;/li&gt;
&lt;li&gt;The refund transaction reference.&lt;/li&gt;
&lt;li&gt;The cashier or manager approving the action.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A cash refund should reduce the expected drawer balance. A card refund should not reduce the physical cash balance.&lt;/p&gt;

&lt;p&gt;This distinction is essential for accurate shift reconciliation.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Reliable Shift-Close Workflow
&lt;/h2&gt;

&lt;p&gt;A structured closing process can reduce disputes and improve financial visibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Stop new transactions for the closing shift
&lt;/h3&gt;

&lt;p&gt;The system should establish a clear cutoff point. Transactions started before closure should either complete or be handled through a controlled pending-transaction process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Calculate expected cash
&lt;/h3&gt;

&lt;p&gt;The calculation should consider the opening float, cash sales, cash refunds, cash additions and cash payouts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Enter the physical count
&lt;/h3&gt;

&lt;p&gt;The cashier counts the actual drawer balance and enters the denomination-wise totals if the business uses that process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Compare expected and counted amounts
&lt;/h3&gt;

&lt;p&gt;The system calculates the variance and displays the difference clearly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Record explanations and approvals
&lt;/h3&gt;

&lt;p&gt;If the drawer is short or over, the cashier should provide a reason. High-value discrepancies may require manager approval.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Lock the shift record
&lt;/h3&gt;

&lt;p&gt;Once approved, the shift should become read-only for ordinary users. Any correction should create an auditable adjustment rather than changing the original closing record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevent Duplicate Financial Events
&lt;/h2&gt;

&lt;p&gt;Network interruptions can cause duplicate payment requests or repeated refund submissions. A POS backend should use idempotency keys or equivalent transaction controls to prevent the same request from being processed more than once.&lt;/p&gt;

&lt;p&gt;The system should also distinguish between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Payment initiated&lt;/li&gt;
&lt;li&gt;Payment authorized&lt;/li&gt;
&lt;li&gt;Payment captured&lt;/li&gt;
&lt;li&gt;Refund requested&lt;/li&gt;
&lt;li&gt;Refund completed&lt;/li&gt;
&lt;li&gt;Refund failed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This prevents a pending refund from being treated as completed before confirmation from the payment provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and Audit Requirements
&lt;/h2&gt;

&lt;p&gt;Cash reconciliation data should be protected through role-based access control, encrypted communication, audit logs and controlled approval workflows.&lt;/p&gt;

&lt;p&gt;Cashiers should not be able to edit completed shifts or remove transaction history. Managers should have access to discrepancy reports, while administrators should be able to review changes across stores and terminals.&lt;/p&gt;

&lt;p&gt;For businesses handling card payments, payment processing should also follow the applicable PCI DSS requirements and provider integration guidelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended POS Software Development Companies
&lt;/h2&gt;

&lt;p&gt;Businesses planning a reconciliation-focused POS solution should evaluate providers on transaction integrity, payment integrations, reporting capabilities and long-term support.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Dev Technosys
&lt;/h3&gt;

&lt;p&gt;Dev Technosys provides custom software and application development services, including solutions that can be designed around retail workflows, payment processing and business-specific operational requirements. Its experience with custom development makes it a relevant option for businesses planning POS software development with tailored reconciliation, user permissions and reporting features.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. NCR Voyix
&lt;/h3&gt;

&lt;p&gt;NCR Voyix offers retail and restaurant technology solutions, including point-of-sale capabilities. Its retail technology background makes it relevant to businesses evaluating established POS infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Oracle
&lt;/h3&gt;

&lt;p&gt;Oracle provides enterprise retail and point-of-sale technology through its retail solutions portfolio. Its offerings are relevant to larger businesses managing complex store operations and connected enterprise systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Square
&lt;/h3&gt;

&lt;p&gt;Square provides POS products and payment-related tools for businesses. Its ecosystem is relevant to smaller and growing retailers that want connected checkout and payment operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Cash drawer reconciliation should explain every financial movement from the beginning of a shift to its closure. Voids, refunds, payouts and corrections must remain distinguishable, traceable and linked to the appropriate transactions.&lt;/p&gt;

&lt;p&gt;When businesses plan &lt;a href="https://devtechnosys.com/pos-software-development.php" rel="noopener noreferrer"&gt;POS software development&lt;/a&gt;, they should treat reconciliation as a core financial workflow rather than a report added at the end. A well-designed ledger, controlled shift closure and reliable payment integration can make discrepancies easier to investigate and prevent repeated operational errors.&lt;/p&gt;

</description>
      <category>pos</category>
      <category>payments</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>Last-Mile ETA Drift: Recalculating Delivery Windows When Dwell Time and Failed Stops Break the Route Plan</title>
      <dc:creator>Jessica Williams</dc:creator>
      <pubDate>Thu, 05 Nov 2026 06:26:37 +0000</pubDate>
      <link>https://dev.to/williamsj04/last-mile-eta-drift-recalculating-delivery-windows-when-dwell-time-and-failed-stops-break-the-19pp</link>
      <guid>https://dev.to/williamsj04/last-mile-eta-drift-recalculating-delivery-windows-when-dwell-time-and-failed-stops-break-the-19pp</guid>
      <description>&lt;p&gt;A delivery route can look perfect when it leaves the depot. The driver has an optimized sequence of stops, estimated arrival times, assigned delivery windows, and a planned return time. Yet by the middle of the route, the schedule may already be inaccurate.&lt;/p&gt;

&lt;p&gt;A customer takes longer than expected to unload. A warehouse delays vehicle entry. One delivery fails because nobody is available to receive the shipment. The driver spends another twenty minutes finding an alternative unloading point. Suddenly, the estimated arrival time for the next customer is no longer reliable.&lt;/p&gt;

&lt;p&gt;This is last-mile ETA drift: the difference between the arrival time predicted by a logistics system and the arrival time that is becoming realistic as the delivery progresses.&lt;/p&gt;

&lt;p&gt;For logistics platforms, the challenge is not simply recalculating the route whenever a vehicle moves. A reliable ETA engine must understand dwell time, failed delivery attempts, stop dependencies, traffic conditions, and the remaining work on the driver's route.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Last-Mile ETAs Become Inaccurate
&lt;/h2&gt;

&lt;p&gt;Traditional route planning typically starts with a set of known inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vehicle starting location&lt;/li&gt;
&lt;li&gt;Destination coordinates&lt;/li&gt;
&lt;li&gt;Planned stop sequence&lt;/li&gt;
&lt;li&gt;Estimated travel time&lt;/li&gt;
&lt;li&gt;Customer delivery windows&lt;/li&gt;
&lt;li&gt;Vehicle capacity and route constraints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These inputs establish an initial schedule. However, they do not always represent what happens at each stop.&lt;/p&gt;

&lt;p&gt;Consider a vehicle with eight scheduled deliveries. The first four stops are completed, but the second customer takes 35 minutes instead of the expected 15. The fifth stop has a restricted receiving window, and the sixth customer is unavailable.&lt;/p&gt;

&lt;p&gt;The original ETA calculations may still use the planned service duration for each stop. As a result, the customer-facing arrival estimates gradually become disconnected from actual operating conditions.&lt;/p&gt;

&lt;p&gt;Several factors contribute to this problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Dwell Time Is Different at Every Stop
&lt;/h3&gt;

&lt;p&gt;Dwell time is the period a vehicle spends at a location after arriving. It may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Security checks and gate entry&lt;/li&gt;
&lt;li&gt;Dock assignment&lt;/li&gt;
&lt;li&gt;Unloading and loading&lt;/li&gt;
&lt;li&gt;Document verification&lt;/li&gt;
&lt;li&gt;Customer handover&lt;/li&gt;
&lt;li&gt;Signature collection&lt;/li&gt;
&lt;li&gt;Waiting for receiving staff&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A route planner may use a standard service duration, but actual dwell time varies by location, customer, shipment type, and time of day.&lt;/p&gt;

&lt;p&gt;A distribution center may consistently take longer during its morning receiving window. A retail location may have a short unloading process but a long security check.&lt;/p&gt;

&lt;p&gt;If the ETA engine treats every stop as having the same service duration, it will repeatedly underestimate downstream arrival times.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Failed Stops Create Additional Work
&lt;/h3&gt;

&lt;p&gt;A failed delivery changes more than the status of one stop.&lt;/p&gt;

&lt;p&gt;The driver may need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Record the failure reason&lt;/li&gt;
&lt;li&gt;Contact the dispatcher&lt;/li&gt;
&lt;li&gt;Attempt customer communication&lt;/li&gt;
&lt;li&gt;Find a secure location for the shipment&lt;/li&gt;
&lt;li&gt;Return the delivery to the depot&lt;/li&gt;
&lt;li&gt;Reschedule the stop&lt;/li&gt;
&lt;li&gt;Complete another delivery attempt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A failed stop can also affect vehicle capacity, route priority, and customer commitments.&lt;/p&gt;

&lt;p&gt;The ETA engine should therefore treat a failed delivery as an operational event that changes the remaining route, not merely as a status update.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Traffic Conditions Change During Execution
&lt;/h3&gt;

&lt;p&gt;Traffic estimates generated before departure may become outdated. Road closures, congestion, weather, and local restrictions can change the time required to reach a destination.&lt;/p&gt;

&lt;p&gt;A route engine should distinguish between planned travel time and updated travel time. It should also avoid assuming that every delay can be recovered by changing the route sequence.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. GPS Data May Arrive Late
&lt;/h3&gt;

&lt;p&gt;A mobile device can lose connectivity, and telematics data may be delayed. If the backend receives location updates after the actual observation time, it may calculate an ETA using an outdated vehicle position.&lt;/p&gt;

&lt;p&gt;A reliable system should retain both the location observation timestamp and the server receipt timestamp. This allows the ETA engine to recognize stale data instead of presenting an old location as current.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build ETA Recalculation Around Actual Route Events
&lt;/h2&gt;

&lt;p&gt;A useful ETA architecture combines planned route information with events generated during execution.&lt;/p&gt;

&lt;p&gt;The key events may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vehicle departed from the depot&lt;/li&gt;
&lt;li&gt;Vehicle arrived at a stop&lt;/li&gt;
&lt;li&gt;Service started&lt;/li&gt;
&lt;li&gt;Service completed&lt;/li&gt;
&lt;li&gt;Delivery failed&lt;/li&gt;
&lt;li&gt;Driver reported an exception&lt;/li&gt;
&lt;li&gt;Vehicle departed from the stop&lt;/li&gt;
&lt;li&gt;Route was reassigned or reordered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each event should be associated with a vehicle, trip, stop, timestamp, and source.&lt;/p&gt;

&lt;p&gt;The backend can then maintain the actual status of the route and recalculate the remaining schedule whenever a meaningful event changes the expected workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate Travel Time From Service Time
&lt;/h2&gt;

&lt;p&gt;One of the most important design decisions is to calculate travel time and dwell time separately.&lt;/p&gt;

&lt;p&gt;A simplified model is:&lt;/p&gt;

&lt;p&gt;ETA(n) = T(current) + sum of [Travel(i) + Service(i)] for every stop i from the current stop up to stop n-1&lt;/p&gt;

&lt;p&gt;Where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;T(current) is the current time or latest reliable calculation point.&lt;/li&gt;
&lt;li&gt;Travel(i) is the estimated travel time to the next stop.&lt;/li&gt;
&lt;li&gt;Service(i) is the expected dwell or service duration at that stop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model becomes more useful when actual dwell time replaces the planned duration for completed stops and updated estimates are applied to the remaining stops.&lt;/p&gt;

&lt;p&gt;For example, if a vehicle arrives at a warehouse at 10:00 a.m. and leaves at 10:42 a.m., the system should use the actual 42-minute dwell time when calculating the next delivery's ETA.&lt;/p&gt;

&lt;p&gt;It should not continue using the original 15-minute assumption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Historical Dwell Time Carefully
&lt;/h2&gt;

&lt;p&gt;Historical dwell-time data can improve estimates, but it needs context.&lt;/p&gt;

&lt;p&gt;A simple average across all stops may hide important differences. A large warehouse, a residential delivery, and a retail receiving dock do not have comparable service patterns.&lt;/p&gt;

&lt;p&gt;A better model can segment historical data by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customer or delivery location&lt;/li&gt;
&lt;li&gt;Stop type&lt;/li&gt;
&lt;li&gt;Shipment category&lt;/li&gt;
&lt;li&gt;Vehicle type&lt;/li&gt;
&lt;li&gt;Time of day&lt;/li&gt;
&lt;li&gt;Day of week&lt;/li&gt;
&lt;li&gt;Delivery service requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system should also account for unusual observations. One exceptionally long unloading event should not automatically become the expected dwell time for every future visit.&lt;/p&gt;

&lt;p&gt;Median dwell time, percentile-based estimates, or carefully maintained historical ranges may be more useful than an unfiltered average.&lt;/p&gt;

&lt;p&gt;For example, a dispatcher may need to see both a likely arrival time and a realistic arrival window when the destination has variable unloading delays.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle Failed Deliveries as Explicit Route Changes
&lt;/h2&gt;

&lt;p&gt;A failed stop should trigger a defined workflow.&lt;/p&gt;

&lt;p&gt;First, the driver records the failure reason. The backend validates the event and updates the stop status. The route engine then determines which future stops are affected.&lt;/p&gt;

&lt;p&gt;Depending on the operational rules, the system may:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep the failed stop in the route for a later retry.&lt;/li&gt;
&lt;li&gt;Move the failed stop to the end of the route.&lt;/li&gt;
&lt;li&gt;Schedule a return to the depot.&lt;/li&gt;
&lt;li&gt;Assign the delivery to another vehicle.&lt;/li&gt;
&lt;li&gt;Request dispatcher approval before changing the route.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Not every failed delivery should be automatically rescheduled. A refrigerated shipment, a high-value parcel, and a time-sensitive medical delivery may require different handling.&lt;/p&gt;

&lt;p&gt;The ETA engine must also avoid counting a failed stop as completed service if the driver has not actually finished the required work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recalculate Without Creating Notification Noise
&lt;/h2&gt;

&lt;p&gt;Frequent ETA changes can frustrate customers and dispatchers. If the system sends a new arrival notification after every small GPS movement, recipients may stop paying attention.&lt;/p&gt;

&lt;p&gt;A better notification policy considers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether the ETA change is operationally meaningful&lt;/li&gt;
&lt;li&gt;Whether the customer has already received an update&lt;/li&gt;
&lt;li&gt;Whether the delivery window is at risk&lt;/li&gt;
&lt;li&gt;Whether the new estimate is based on reliable data&lt;/li&gt;
&lt;li&gt;Whether a dispatcher needs to approve the change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, a two-minute adjustment may not require a customer notification. A 30-minute delay that threatens a booked receiving window probably does.&lt;/p&gt;

&lt;p&gt;Use notification thresholds and event-based rules, while retaining the full ETA history for operational review.&lt;/p&gt;

&lt;h2&gt;
  
  
  An Example of ETA Drift in a Delivery Route
&lt;/h2&gt;

&lt;p&gt;Imagine a vehicle leaves the depot at 8:00 a.m. with six scheduled deliveries.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stop&lt;/th&gt;
&lt;th&gt;Planned arrival&lt;/th&gt;
&lt;th&gt;Actual event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;8:30 a.m.&lt;/td&gt;
&lt;td&gt;Completed at 8:42 a.m.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;9:10 a.m.&lt;/td&gt;
&lt;td&gt;Completed at 9:48 a.m.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;10:15 a.m.&lt;/td&gt;
&lt;td&gt;Customer unavailable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;10:50 a.m.&lt;/td&gt;
&lt;td&gt;Still pending&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;11:30 a.m.&lt;/td&gt;
&lt;td&gt;Still pending&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;12:15 p.m.&lt;/td&gt;
&lt;td&gt;Still pending&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The vehicle has already lost time at the second stop and has encountered a failed delivery at the third.&lt;/p&gt;

&lt;p&gt;A basic system may continue displaying the original arrival estimates. A more capable system updates the remaining route using actual departure time, current vehicle location, traffic, and the failed-stop policy.&lt;/p&gt;

&lt;p&gt;The dispatcher can then decide whether to keep the original sequence, move the failed delivery, or reassign a time-sensitive stop.&lt;/p&gt;

&lt;p&gt;The important outcome is not simply a more accurate number. It is a schedule that reflects the actual operational situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Logistics Software Development Services Should Include
&lt;/h2&gt;

&lt;p&gt;An ETA engine is only one component of a dependable delivery platform. Logistics software development services should also address the systems that supply and consume route information.&lt;/p&gt;

&lt;p&gt;A complete implementation may require:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPS and telematics integrations&lt;/li&gt;
&lt;li&gt;Route planning and optimization&lt;/li&gt;
&lt;li&gt;Driver mobile applications&lt;/li&gt;
&lt;li&gt;Stop-level event tracking&lt;/li&gt;
&lt;li&gt;Dwell-time analytics&lt;/li&gt;
&lt;li&gt;Failed delivery workflows&lt;/li&gt;
&lt;li&gt;Customer notifications&lt;/li&gt;
&lt;li&gt;Dispatcher dashboards&lt;/li&gt;
&lt;li&gt;Delivery proof and document capture&lt;/li&gt;
&lt;li&gt;Exception management&lt;/li&gt;
&lt;li&gt;Historical route reporting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture should also support idempotent event processing. If a driver's device retries a delivery-completion event after a network timeout, the backend should not mark the same stop as completed twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Logistics Software Development Companies
&lt;/h2&gt;

&lt;p&gt;Dev Technosys develops custom logistics and mobile applications that can connect route planning, driver workflows, tracking data, delivery status, and operational dashboards. A project involving ETA drift should define stop-level event handling, dwell-time rules, failed-delivery workflows, and notification thresholds as part of the backend scope.&lt;/p&gt;

&lt;p&gt;IBM offers enterprise technology and data capabilities that may be relevant when logistics operations need to connect route information with broader enterprise systems and analytics platforms.&lt;/p&gt;

&lt;p&gt;Accenture can be considered for larger logistics transformation initiatives involving process redesign, enterprise integration, and application modernization.&lt;/p&gt;

&lt;p&gt;When selecting a development partner, ask how its proposed architecture will handle delayed GPS updates, repeated events, failed deliveries, and customer-facing ETA changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Last-mile ETA drift is not just a routing problem. It is the result of differences between planned operations and actual events.&lt;/p&gt;

&lt;p&gt;A dependable logistics platform must understand when a vehicle arrives, how long it stays, whether a delivery succeeds, and what that means for every remaining stop.&lt;/p&gt;

&lt;p&gt;By combining real-time event processing, contextual dwell-time estimates, explicit failed-stop workflows, and controlled notifications, logistics teams can provide more useful arrival estimates and make better decisions when the original route plan no longer reflects reality.&lt;/p&gt;

</description>
      <category>logistics</category>
      <category>backend</category>
      <category>architecture</category>
      <category>algorithms</category>
    </item>
    <item>
      <title>Fleet Maintenance Alerts Fire Twice: Deduplicating OBD-II Fault Codes in a Telematics Backend</title>
      <dc:creator>Jessica Williams</dc:creator>
      <pubDate>Tue, 03 Nov 2026 07:17:39 +0000</pubDate>
      <link>https://dev.to/williamsj04/fleet-maintenance-alerts-fire-twice-deduplicating-obd-ii-fault-codes-in-a-telematics-backend-1aac</link>
      <guid>https://dev.to/williamsj04/fleet-maintenance-alerts-fire-twice-deduplicating-obd-ii-fault-codes-in-a-telematics-backend-1aac</guid>
      <description>&lt;p&gt;A fleet maintenance alert should help a technician decide what needs attention. It should not force the maintenance team to investigate the same engine fault three times because a telematics device retransmitted a message, a vehicle reported the same diagnostic code through two channels, or a backend retry created a duplicate notification.&lt;/p&gt;

&lt;p&gt;This problem often appears when a fleet management platform begins connecting vehicles from different manufacturers, aftermarket OBD-II devices, and telematics providers. The initial implementation may work well with a small number of vehicles. As the number of connected assets grows, duplicate fault events can overwhelm dashboards, inflate notification counts, and weaken trust in automated maintenance workflows.&lt;/p&gt;

&lt;p&gt;The solution is not simply to ignore repeated diagnostic trouble codes. A reliable backend needs to distinguish duplicate messages from recurring faults, continuing faults, and truly new incidents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OBD-II Fault Codes Generate Duplicate Alerts
&lt;/h2&gt;

&lt;p&gt;OBD-II systems expose diagnostic information that can help fleet operators identify engine and emissions-related problems. Depending on the vehicle, diagnostic interface, and telematics provider, the backend may receive fault information at different intervals or through different event formats.&lt;/p&gt;

&lt;p&gt;Several conditions can produce repeated messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Device retransmission&lt;/strong&gt;: A tracker temporarily loses network connectivity and sends buffered messages after reconnecting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Webhook retries&lt;/strong&gt;: A telematics provider retries a delivery because the receiving API did not acknowledge the original request within the expected time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Polling overlap&lt;/strong&gt;: A backend polls the same vehicle repeatedly and receives an unchanged diagnostic code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple data sources&lt;/strong&gt;: A vehicle's OEM telematics feed and an aftermarket device report the same underlying fault.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repeated fault observations&lt;/strong&gt;: The vehicle continues reporting an active code over multiple trips. These observations may represent one ongoing incident rather than multiple new maintenance cases.&lt;/p&gt;

&lt;p&gt;The challenge is that these situations can look similar at the message level while requiring different treatment in the maintenance system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start With Event Identity, Not Notification Logic
&lt;/h2&gt;

&lt;p&gt;A common implementation checks whether a diagnostic code already exists for a vehicle. If it does, the backend discards the incoming event.&lt;/p&gt;

&lt;p&gt;That approach can suppress useful information.&lt;/p&gt;

&lt;p&gt;Consider a vehicle that reports a diagnostic code on Monday. The technician investigates and closes the maintenance case on Tuesday. The same code appears again on Friday.&lt;/p&gt;

&lt;p&gt;If the system permanently deduplicates that vehicle-code combination, it may suppress the Friday incident even though the fault has returned.&lt;/p&gt;

&lt;p&gt;A better design separates the raw event, the fault state, the maintenance incident, and the notification.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Raw event&lt;/strong&gt;: The original diagnostic observation received from a device or provider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fault state&lt;/strong&gt;: The backend's current understanding of the code for a specific vehicle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance incident&lt;/strong&gt;: The actionable case assigned to a technician or maintenance team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notification&lt;/strong&gt;: The message delivered to the people responsible for responding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation allows the system to retain every useful observation without creating a new maintenance case or sending another notification for every repeated message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a Layered Deduplication Strategy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Normalize incoming diagnostic events
&lt;/h3&gt;

&lt;p&gt;Before deduplication, convert incoming messages into a consistent internal structure.&lt;/p&gt;

&lt;p&gt;A normalized event may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fleet and vehicle identifiers&lt;/li&gt;
&lt;li&gt;VIN, when available and verified&lt;/li&gt;
&lt;li&gt;Telematics device identifier&lt;/li&gt;
&lt;li&gt;Source provider and reporting module&lt;/li&gt;
&lt;li&gt;Diagnostic trouble code&lt;/li&gt;
&lt;li&gt;Code status, where supported&lt;/li&gt;
&lt;li&gt;Source event timestamp&lt;/li&gt;
&lt;li&gt;Backend receipt timestamp&lt;/li&gt;
&lt;li&gt;Device sequence number, when available&lt;/li&gt;
&lt;li&gt;Relevant diagnostic data and source payload&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Normalization matters because different providers may represent the same diagnostic code differently. The backend should preserve the original payload while mapping the event into a common format.&lt;/p&gt;

&lt;p&gt;Do not assume the device identifier, vehicle identifier, and VIN are interchangeable. Devices can be replaced, vehicles can change ownership, and hardware can be reassigned.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use idempotency keys for message retries
&lt;/h3&gt;

&lt;p&gt;When a provider supplies a stable event ID or sequence number, use it to identify repeat deliveries of the same message.&lt;/p&gt;

&lt;p&gt;An idempotency key can be constructed from trusted source fields, such as the provider event ID and source account. If the provider does not supply a stable ID, a carefully designed fingerprint may combine the vehicle, source, diagnostic code, event time, and relevant status fields.&lt;/p&gt;

&lt;p&gt;The fingerprint must reflect the source's actual behavior. A timestamp alone is rarely sufficient because devices may have clock drift, and several distinct events can occur close together.&lt;/p&gt;

&lt;p&gt;Store processed event identifiers with a uniqueness constraint. When the same event arrives again, the backend should acknowledge it without repeating the business operation.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Track active fault episodes
&lt;/h3&gt;

&lt;p&gt;Message deduplication cannot determine whether a fault is still active or has returned after resolution. That requires a state model.&lt;/p&gt;

&lt;p&gt;For each vehicle and normalized fault identity, maintain a record containing fields such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current status&lt;/li&gt;
&lt;li&gt;First observed time&lt;/li&gt;
&lt;li&gt;Last observed time&lt;/li&gt;
&lt;li&gt;Last notification time&lt;/li&gt;
&lt;li&gt;Associated maintenance case&lt;/li&gt;
&lt;li&gt;Resolution or clear time, where known&lt;/li&gt;
&lt;li&gt;Source observations and event history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repeated observations can update the last-seen timestamp and diagnostic history. They do not automatically need to create a new incident.&lt;/p&gt;

&lt;p&gt;When a fault is confirmed as cleared and later reappears, the system can open a new episode according to the relevant code's rules.&lt;/p&gt;

&lt;p&gt;A missing observation should not automatically mean a fault has cleared. A disconnected device may simply have stopped sending data.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Separate fault severity from duplicate suppression
&lt;/h3&gt;

&lt;p&gt;Not all diagnostic codes deserve the same notification policy. A recurring, low-priority warning and a potentially safety-critical fault should not be handled identically.&lt;/p&gt;

&lt;p&gt;Use code-specific rules to determine whether the system should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Update an existing incident silently&lt;/li&gt;
&lt;li&gt;Notify the assigned technician&lt;/li&gt;
&lt;li&gt;Escalate an unresolved incident&lt;/li&gt;
&lt;li&gt;Reopen a resolved case&lt;/li&gt;
&lt;li&gt;Create a new maintenance incident&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deduplication should reduce repetitive noise, not hide important changes in severity or status.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make Event Processing Safe Under Retries
&lt;/h2&gt;

&lt;p&gt;A telematics backend often has several asynchronous stages: event ingestion, normalization, persistence, rule evaluation, notification dispatch, and maintenance-system integration.&lt;/p&gt;

&lt;p&gt;If the same message is processed by two workers simultaneously, both may decide that no incident exists and create one.&lt;/p&gt;

&lt;p&gt;Use database transactions, unique constraints, or another concurrency-safe mechanism to enforce the incident-creation rule. A distributed lock can be useful in specific architectures, but it should not replace durable data-level safeguards.&lt;/p&gt;

&lt;p&gt;Notification delivery needs its own idempotency mechanism. Creating one maintenance incident does not guarantee that an email, push notification, or webhook will be delivered only once.&lt;/p&gt;

&lt;p&gt;For reliable processing, store notification intent in a transactional outbox or equivalent durable queue. A worker can dispatch the notification and record its delivery state. Since external delivery systems may still retry, use a stable notification key wherever the destination supports it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: One Fault, Multiple Messages
&lt;/h2&gt;

&lt;p&gt;Suppose a delivery truck reports diagnostic code P0420 through an aftermarket telematics device. The device sends the same buffered message twice, and the provider retries the webhook once.&lt;/p&gt;

&lt;p&gt;The backend receives three messages.&lt;/p&gt;

&lt;p&gt;A robust implementation would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Persist or identify the original event.&lt;/li&gt;
&lt;li&gt;Recognize repeated deliveries using the event ID or fingerprint.&lt;/li&gt;
&lt;li&gt;Maintain one active fault episode.&lt;/li&gt;
&lt;li&gt;Create one maintenance incident if the configured rules require it.&lt;/li&gt;
&lt;li&gt;Dispatch the initial notification once.&lt;/li&gt;
&lt;li&gt;Preserve later observations and retry details for audit purposes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the fault is cleared and subsequently returns, the backend evaluates the new evidence and may create another incident.&lt;/p&gt;

&lt;p&gt;The important distinction is that three deliveries do not necessarily mean three faults, while three observations do not always mean one fault forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the Cases That Break Naive Deduplication
&lt;/h2&gt;

&lt;p&gt;Before deploying the logic across a fleet, test more than a simple repeated-message scenario.&lt;/p&gt;

&lt;p&gt;Include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same event delivered several times&lt;/li&gt;
&lt;li&gt;The same code reported with different timestamps&lt;/li&gt;
&lt;li&gt;Two providers reporting the same fault&lt;/li&gt;
&lt;li&gt;Two distinct faults arriving almost simultaneously&lt;/li&gt;
&lt;li&gt;A delayed message arriving after a newer event&lt;/li&gt;
&lt;li&gt;A fault disappearing from the feed without an explicit clear event&lt;/li&gt;
&lt;li&gt;A cleared fault returning days later&lt;/li&gt;
&lt;li&gt;Concurrent workers processing the same vehicle&lt;/li&gt;
&lt;li&gt;A notification provider timing out after accepting a message&lt;/li&gt;
&lt;li&gt;A vehicle being reassigned to a different telematics device&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Track duplicate-event rates, incidents created per fault episode, notification retries, processing delays, and faults reopened after resolution. These metrics help determine whether deduplication is reducing noise without hiding real maintenance needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Fleet Management App Development Should Include
&lt;/h2&gt;

&lt;p&gt;Fleet management app development is not limited to a map, a vehicle list, and a maintenance reminder screen. When the product consumes telematics data, its backend needs reliable event processing, fault history, configurable escalation rules, and connections to work orders and technician assignments.&lt;/p&gt;

&lt;p&gt;A development team should understand the difference between raw telemetry and actionable operational events. It should also plan for mixed hardware, inconsistent provider payloads, intermittent connectivity, and data ownership across fleet accounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Fleet Software Development Companies
&lt;/h2&gt;

&lt;p&gt;Dev Technosys is a &lt;a href="https://devtechnosys.com/fleet-management-app-development.php" rel="noopener noreferrer"&gt;fleet management software development company&lt;/a&gt; that builds custom fleet and logistics software, including applications that can connect operational workflows with telematics integrations, vehicle maintenance records, and management dashboards. Its 250+ professionals support software delivery across different business requirements. The project scope should explicitly define diagnostic event handling, integration responsibilities, and fault lifecycle rules.&lt;/p&gt;

&lt;p&gt;IBM provides enterprise technology and integration capabilities that can support complex data and operational systems. Its enterprise engineering ecosystem can be relevant when telematics needs to connect with existing business platforms and analytics infrastructure.&lt;/p&gt;

&lt;p&gt;Accenture works across enterprise software, cloud, data, and systems integration. Its delivery capabilities can be relevant to organizations modernizing fleet operations across several departments or existing technology platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Duplicate OBD-II alerts are not merely a notification problem. They expose how a telematics backend identifies events, maintains fault state, and coordinates work across distributed systems.&lt;/p&gt;

&lt;p&gt;The most reliable approach combines source-level idempotency, normalized diagnostic data, fault episode tracking, concurrency-safe incident creation, and independently controlled notification delivery.&lt;/p&gt;

&lt;p&gt;The goal is not to suppress every repeated code. It is to make sure every message contributes to a trustworthy maintenance record while each actionable incident receives the appropriate response.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>architecture</category>
      <category>iot</category>
      <category>devops</category>
    </item>
    <item>
      <title>EDI to API Migration for Supply Chain Software: Running Both Without Dropping Purchase Orders</title>
      <dc:creator>Jessica Williams</dc:creator>
      <pubDate>Mon, 02 Nov 2026 07:27:35 +0000</pubDate>
      <link>https://dev.to/williamsj04/edi-to-api-migration-for-supply-chain-software-running-both-without-dropping-purchase-orders-54hj</link>
      <guid>https://dev.to/williamsj04/edi-to-api-migration-for-supply-chain-software-running-both-without-dropping-purchase-orders-54hj</guid>
      <description>&lt;p&gt;Migrating from Electronic Data Interchange (EDI) to APIs sounds like a simple modernization project. Replace file exchanges with real-time endpoints, connect supplier systems, and eliminate delays.&lt;/p&gt;

&lt;p&gt;The difficult part begins when both methods must operate simultaneously.&lt;/p&gt;

&lt;p&gt;A manufacturer may still receive purchase orders through EDI from established retail customers while newer suppliers expect API-based integration. During the transition, the same order may arrive through both channels, an acknowledgement may be delayed, or an API request may fail after the ERP has already created the purchase order.&lt;/p&gt;

&lt;p&gt;If the migration does not account for these conditions, the business can end up with missing orders, duplicate purchase orders, incorrect acknowledgements, or conflicting order statuses.&lt;/p&gt;

&lt;p&gt;Reliable supply chain software development requires a migration strategy that preserves business transactions while interfaces and integration patterns change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Supply Chain EDI Migration Is Different
&lt;/h2&gt;

&lt;p&gt;EDI remains important in supply chains because trading partners often depend on established document standards and partner-specific implementation rules. Common documents include purchase orders, purchase-order acknowledgements, advance shipping notices, and invoices.&lt;/p&gt;

&lt;p&gt;APIs can offer more immediate communication, structured request-response interactions, and clearer integration contracts. However, an API does not automatically replace every EDI workflow. Partners may use different data formats, validation rules, identifiers, and acknowledgement expectations.&lt;/p&gt;

&lt;p&gt;The migration must preserve business meaning across both channels.&lt;/p&gt;

&lt;p&gt;For example, an EDI purchase order and an API purchase order may represent the same commercial transaction even though their payloads and transport mechanisms differ. The integration layer must recognize that relationship rather than treating the messages as unrelated requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design a Canonical Purchase-Order Model
&lt;/h2&gt;

&lt;p&gt;Before running both channels, define a shared internal representation of a purchase order.&lt;/p&gt;

&lt;p&gt;The model should include the business identifiers and fields required by the organization, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Buyer and supplier identifiers&lt;/li&gt;
&lt;li&gt;Purchase-order number and revision&lt;/li&gt;
&lt;li&gt;Order date and requested delivery date&lt;/li&gt;
&lt;li&gt;Product identifiers and quantities&lt;/li&gt;
&lt;li&gt;Units of measure&lt;/li&gt;
&lt;li&gt;Ship-to and bill-to locations&lt;/li&gt;
&lt;li&gt;Currency, pricing, and applicable terms&lt;/li&gt;
&lt;li&gt;Document and processing status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The EDI adapter translates incoming documents into this model. The API adapter validates incoming requests and maps them to the same representation.&lt;/p&gt;

&lt;p&gt;This approach reduces the risk of maintaining two independent business implementations. Validation, order creation, authorization, and downstream processing can follow a consistent set of rules regardless of the source channel.&lt;/p&gt;

&lt;p&gt;The canonical model should not erase partner-specific requirements. EDI segments, qualifiers, codes, and API fields may need explicit mappings and validation rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Biggest Risk: One Order Arriving Twice
&lt;/h2&gt;

&lt;p&gt;Consider a supplier that sends a purchase order through EDI while testing a new API connection. Both messages reach the integration platform.&lt;/p&gt;

&lt;p&gt;If each channel creates a purchase order independently, the ERP may receive two records for one commercial order. The duplicate can trigger repeated procurement work, incorrect inventory expectations, or conflicting supplier communications.&lt;/p&gt;

&lt;p&gt;A safer design separates message identity from business identity.&lt;/p&gt;

&lt;p&gt;Message identity identifies a specific transmission or delivery attempt. Business identity identifies the commercial order that the message represents.&lt;/p&gt;

&lt;p&gt;The integration should use stable identifiers, document revisions, partner-specific rules, and business validation to determine whether a request is new, a retry, or an update to an existing order.&lt;/p&gt;

&lt;p&gt;A simple rule that rejects every repeated purchase-order number is not sufficient. Some partners legitimately revise orders, reuse identifiers according to agreed conventions, or send replacement documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running EDI and APIs in Parallel
&lt;/h2&gt;

&lt;p&gt;A controlled dual-running strategy is usually safer than switching every trading partner at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Inventory the Current Integrations
&lt;/h3&gt;

&lt;p&gt;Document every trading partner, EDI document type, message direction, transformation rule, acknowledgement, and downstream system dependency.&lt;/p&gt;

&lt;p&gt;Identify which integrations are business-critical and which partners are ready for API-based communication.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: Build the Shared Integration Layer
&lt;/h3&gt;

&lt;p&gt;Create adapters for EDI and APIs that feed a common validation and order-processing workflow. Establish consistent logging, business identifiers, error handling, and transaction-state management.&lt;/p&gt;

&lt;p&gt;Avoid building separate order-creation logic for each channel unless a real business difference requires it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3: Introduce Controlled Partner Migration
&lt;/h3&gt;

&lt;p&gt;Move a limited set of suppliers or customers to the API path. Define which channel is authoritative for each partner and document type.&lt;/p&gt;

&lt;p&gt;During parallel testing, compare results without allowing both paths to create independent production orders. Shadow processing can validate API mappings against existing EDI traffic, provided the test path cannot trigger duplicate downstream actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 4: Monitor and Reconcile
&lt;/h3&gt;

&lt;p&gt;Track orders received, validated, created, acknowledged, rejected, and left pending. Reconcile inbound documents against ERP records and outbound acknowledgements against the relevant business transactions.&lt;/p&gt;

&lt;p&gt;The migration should not be declared successful merely because the API returns an HTTP success status.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 5: Retire EDI by Partner and Document Type
&lt;/h3&gt;

&lt;p&gt;Decommission an EDI flow only after the replacement has passed business validation, operational testing, and an agreed observation period. Some partners may continue using EDI for years, so the architecture should support a mixed environment where necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevent Lost Orders During Failures
&lt;/h2&gt;

&lt;p&gt;One of the most dangerous integration scenarios occurs when an API request times out after the ERP has created the purchase order but before the integration service receives confirmation.&lt;/p&gt;

&lt;p&gt;If the service retries blindly, it may create a duplicate. If it marks the order as failed and discards the request, the order may remain unacknowledged.&lt;/p&gt;

&lt;p&gt;Use durable processing records and idempotent operations wherever possible. A stable request or transaction reference allows the system to check whether the original operation already succeeded.&lt;/p&gt;

&lt;p&gt;If the ERP does not support idempotency directly, the integration may need a reliable lookup or reconciliation mechanism based on the agreed business identifiers. The exact solution depends on the ERP's transaction semantics and available APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Acknowledgements Need Their Own State Management
&lt;/h2&gt;

&lt;p&gt;Receiving a purchase order, creating it in the ERP, and acknowledging it to the trading partner are separate stages.&lt;/p&gt;

&lt;p&gt;A system may successfully receive an EDI document but fail to create the order. An API request may create the order while the acknowledgement service is unavailable. These situations need different statuses and recovery procedures.&lt;/p&gt;

&lt;p&gt;A useful lifecycle might include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Received&lt;/li&gt;
&lt;li&gt;Validated&lt;/li&gt;
&lt;li&gt;Accepted for processing&lt;/li&gt;
&lt;li&gt;Created or updated in the ERP&lt;/li&gt;
&lt;li&gt;Acknowledgement pending&lt;/li&gt;
&lt;li&gt;Acknowledgement sent&lt;/li&gt;
&lt;li&gt;Reconciled&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The actual lifecycle should reflect the partner's contractual requirements. The key is to make each stage observable and recoverable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and Operational Monitoring
&lt;/h2&gt;

&lt;p&gt;EDI and API integrations may contain pricing, supplier details, order quantities, and commercially sensitive information. Secure transport, authentication, restricted service accounts, secrets management, and audit logs should be included in the migration plan.&lt;/p&gt;

&lt;p&gt;Monitor API latency, EDI processing delays, validation failures, duplicate detection, acknowledgement backlogs, and reconciliation exceptions. Alerts should distinguish a temporary communication problem from a business transaction that remains unresolved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Supply Chain Software Development Companies
&lt;/h2&gt;

&lt;p&gt;A dual-running migration may require custom adapters, ERP integration, canonical data mapping, and transaction-recovery workflows. Dev Technosys can be considered for projects involving &lt;a href="https://devtechnosys.com/supply-chain-management-software-development.php" rel="noopener noreferrer"&gt;supply chain software development services&lt;/a&gt;, particularly where existing systems need to support both established EDI processes and newer API integrations.&lt;/p&gt;

&lt;p&gt;Organizations should also assess enterprise integration providers and ERP specialists based on their experience with the actual systems, trading partners, and document standards involved. The selection process should focus on transaction integrity, partner onboarding, observability, and migration support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;EDI-to-API migration is not simply a change in how data travels. It changes the integration architecture surrounding purchase orders, acknowledgements, partner validation, and downstream processing.&lt;/p&gt;

&lt;p&gt;A shared business model, controlled dual running, duplicate prevention, durable processing, and transaction-level reconciliation help protect orders during the transition.&lt;/p&gt;

&lt;p&gt;The objective is not to eliminate EDI overnight. It is to modernize the integration environment without losing the reliability that purchasing and fulfillment teams depend on.&lt;/p&gt;

</description>
      <category>supplychain</category>
      <category>api</category>
      <category>integration</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Supply Chain Management Software Development: Handling Late and Duplicate Shipment Events Without Corrupting Inventory</title>
      <dc:creator>Jessica Williams</dc:creator>
      <pubDate>Sat, 31 Oct 2026 13:44:30 +0000</pubDate>
      <link>https://dev.to/williamsj04/supply-chain-management-software-development-handling-late-and-duplicate-shipment-events-without-49e4</link>
      <guid>https://dev.to/williamsj04/supply-chain-management-software-development-handling-late-and-duplicate-shipment-events-without-49e4</guid>
      <description>&lt;p&gt;A shipment event arrives six hours late. Another event reports that the same pallet has been received twice. Meanwhile, the warehouse management system (WMS) has already updated inventory, the enterprise resource planning (ERP) system has posted a receipt, and the transportation management system (TMS) still shows the shipment as in transit.&lt;/p&gt;

&lt;p&gt;This is a common type of distributed-systems problem in supply chain software. The issue is not simply that one system received incorrect data. Multiple applications may process the same business event at different times, leaving the organization with conflicting records of what happened.&lt;/p&gt;

&lt;p&gt;Supply chain management software development must account for delayed messages, duplicate notifications, out-of-order events, partial failures, and retries. Without those controls, a single shipment update can trigger duplicate inventory movements, incorrect available stock, inaccurate financial records, or unnecessary replenishment orders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Shipment Events Arrive Late or More Than Once
&lt;/h2&gt;

&lt;p&gt;A modern supply chain application rarely operates as one isolated system. Warehouse scanners, carrier APIs, ERP platforms, supplier portals, transportation systems, and inventory services exchange events through APIs, message queues, webhooks, or scheduled integrations.&lt;/p&gt;

&lt;p&gt;These connections can fail in several ways.&lt;/p&gt;

&lt;p&gt;A warehouse device may temporarily lose connectivity and upload its events after reconnecting. A carrier webhook may be retried because the receiving service did not acknowledge it in time. A message broker may redeliver a message after a consumer crashes before confirming processing.&lt;/p&gt;

&lt;p&gt;Events can also arrive out of order. For example, a shipment's departure event may reach the integration service before an earlier loading event.&lt;/p&gt;

&lt;p&gt;The receiving application cannot assume that every incoming message is new, current, or ready to apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Idempotent Event Processing
&lt;/h2&gt;

&lt;p&gt;Idempotency means that processing the same event more than once does not produce additional unintended effects.&lt;/p&gt;

&lt;p&gt;Suppose a warehouse receives 100 units of a product. The WMS sends a receipt event, but the acknowledgement is lost. When the sender retries, the receiving service must recognize the repeated event instead of increasing inventory by another 100 units.&lt;/p&gt;

&lt;p&gt;A robust event-processing workflow should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Assign or retain a stable event identifier.&lt;/li&gt;
&lt;li&gt;Validate the source, event type, and required business identifiers.&lt;/li&gt;
&lt;li&gt;Check whether the event has already been processed.&lt;/li&gt;
&lt;li&gt;Apply the permitted state transition and inventory movement.&lt;/li&gt;
&lt;li&gt;Record the processing result.&lt;/li&gt;
&lt;li&gt;Acknowledge the event only after the required transaction is safely committed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A deduplication table or equivalent durable event ledger can record processed event identifiers. Where possible, the deduplication record and inventory update should be committed in the same database transaction.&lt;/p&gt;

&lt;p&gt;This matters because checking for duplicates in one transaction and updating stock in another can create a race condition. Two workers might both decide that an event is new before either records it as processed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do Not Confuse Duplicate Events With Duplicate Business Actions
&lt;/h2&gt;

&lt;p&gt;Two events can describe the same business action without having identical payloads. A carrier might send one notification with a shipment status and another with an updated timestamp. Conversely, two legitimate receipt events may reference the same purchase order but represent different deliveries.&lt;/p&gt;

&lt;p&gt;Therefore, deduplication should not rely only on matching product IDs, shipment IDs, or timestamps.&lt;/p&gt;

&lt;p&gt;The system needs a stable event identity where available, plus business validation. A unique event ID can prevent a repeated message from being applied twice, while a separate business rule can prevent a shipment receipt from exceeding the quantity expected under a particular delivery or purchase order.&lt;/p&gt;

&lt;p&gt;The goal is to eliminate repeated processing without suppressing legitimate movements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle Out-of-Order Events With State Transitions
&lt;/h2&gt;

&lt;p&gt;A shipment usually follows a series of business states: created, allocated, picked, packed, dispatched, in transit, delivered, and received. Actual workflows may contain exceptions, partial deliveries, returns, and cancellations.&lt;/p&gt;

&lt;p&gt;A naive implementation might update the shipment status whenever a new message arrives. That approach can cause a late event to overwrite a more advanced state.&lt;/p&gt;

&lt;p&gt;For example, a delayed IN_TRANSIT event should not automatically replace a confirmed DELIVERED state.&lt;/p&gt;

&lt;p&gt;A safer design validates each transition against the current state and the source event's meaning. The system may use event sequence numbers, source timestamps, version numbers, or authoritative status queries where available. Timestamps alone are not always reliable because clocks can differ and events can be generated at different stages.&lt;/p&gt;

&lt;p&gt;When an event cannot be safely applied, the system should preserve it for review or reconciliation rather than silently discarding it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Inventory Movements Separate From Shipment Status
&lt;/h2&gt;

&lt;p&gt;A shipment status and an inventory movement are related, but they are not interchangeable.&lt;/p&gt;

&lt;p&gt;A carrier reporting that a truck has arrived at a distribution center does not necessarily mean the warehouse has physically received and verified the goods. Similarly, a delivery confirmation may refer to the destination rather than a completed put-away operation.&lt;/p&gt;

&lt;p&gt;Supply chain software should define which business event authorizes each inventory change.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A dispatch confirmation may reduce the stock available at the shipping warehouse.&lt;/li&gt;
&lt;li&gt;A goods-receipt confirmation may increase received stock at the destination.&lt;/li&gt;
&lt;li&gt;A quality-inspection event may move units into a restricted or quarantine state.&lt;/li&gt;
&lt;li&gt;A put-away confirmation may move units from receiving to a storage location.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation prevents a shipment notification from incorrectly changing sellable inventory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Durable Queues and Transactional Outbox Patterns
&lt;/h2&gt;

&lt;p&gt;When one service updates a database and then publishes an event, a failure between those operations can leave the database updated but the message unsent.&lt;/p&gt;

&lt;p&gt;The transactional outbox pattern addresses this by recording the business change and an outgoing event in the same database transaction. A separate publisher then sends the event to the message broker.&lt;/p&gt;

&lt;p&gt;On the receiving side, an inbox or event ledger can track incoming messages and prevent repeated processing.&lt;/p&gt;

&lt;p&gt;These patterns do not remove every failure. They make failures recoverable by ensuring that the system has durable records of the work that still needs to be delivered or processed.&lt;/p&gt;

&lt;p&gt;The architecture should also define retry limits, backoff strategies, dead-letter handling, and procedures for replaying failed messages safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconcile Systems Instead of Assuming They Agree
&lt;/h2&gt;

&lt;p&gt;Even with careful event processing, ERP, WMS, and TMS records can diverge because of configuration errors, integration outages, manual adjustments, or unsupported transitions.&lt;/p&gt;

&lt;p&gt;A reconciliation process should compare the systems' business records using meaningful identifiers such as shipment ID, purchase order, receipt reference, SKU, location, and quantity.&lt;/p&gt;

&lt;p&gt;Useful exception categories include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shipment marked delivered but no warehouse receipt exists.&lt;/li&gt;
&lt;li&gt;A receipt exists in the WMS but not in the ERP.&lt;/li&gt;
&lt;li&gt;The same receipt reference appears more than once.&lt;/li&gt;
&lt;li&gt;Received quantity exceeds the expected delivery quantity.&lt;/li&gt;
&lt;li&gt;Inventory has moved into an unavailable or quarantine state unexpectedly.&lt;/li&gt;
&lt;li&gt;A shipment status is inconsistent with the documented event history.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reconciliation should create actionable exceptions with traceable records. Automatically forcing two systems to match without understanding the discrepancy can hide the underlying problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Failure Scenarios
&lt;/h2&gt;

&lt;p&gt;A supply chain application should be tested against failure conditions, not just normal API responses.&lt;/p&gt;

&lt;p&gt;The test plan should include duplicate webhooks, delayed events, out-of-order messages, worker crashes, database timeouts, message-broker outages, partial receipts, and manual inventory corrections.&lt;/p&gt;

&lt;p&gt;For each scenario, verify that the system preserves the correct inventory balance, records the event outcome, and supports safe recovery. Load testing should also establish how the system behaves when a carrier or warehouse reconnects and sends a backlog of events.&lt;/p&gt;

&lt;p&gt;Useful operational metrics include duplicate-event rates, processing latency, retry counts, dead-letter queue volume, reconciliation exceptions, and the time required to resolve an inconsistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Supply Chain Software Development Companies
&lt;/h2&gt;

&lt;p&gt;Organizations implementing these controls may need custom event-processing services, ERP/WMS/TMS integration, or changes to an existing supply chain platform. Dev Technosys is one company to consider for custom supply chain management software development, particularly where business-specific workflows and system integrations need to be planned together.&lt;/p&gt;

&lt;p&gt;Other options include enterprise technology providers and integration specialists whose experience matches the organization's existing systems. The selection process should verify event-processing design, transaction boundaries, observability, recovery procedures, and responsibility for ongoing integration support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Late and duplicate shipment events are inevitable in distributed supply chain environments. Inventory corruption is not.&lt;/p&gt;

&lt;p&gt;Idempotent processing, durable event records, validated state transitions, explicit inventory rules, and reconciliation make it possible to recover from delayed or repeated messages without creating additional business errors.&lt;/p&gt;

&lt;p&gt;The strongest supply chain architecture treats event delivery as unreliable, records every meaningful transition, and makes uncertainty visible to operations teams. That is the foundation for keeping warehouse inventory, shipment status, and ERP records consistent as the business scales.&lt;/p&gt;

</description>
      <category>supplychain</category>
      <category>softwaredevelopment</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Hotel POS Software Development: How to Split One Guest Bill Across Room, Card and Corporate Accounts</title>
      <dc:creator>Jessica Williams</dc:creator>
      <pubDate>Fri, 30 Oct 2026 10:08:09 +0000</pubDate>
      <link>https://dev.to/williamsj04/hotel-pos-software-development-how-to-split-one-guest-bill-across-room-card-and-corporate-accounts-4e4o</link>
      <guid>https://dev.to/williamsj04/hotel-pos-software-development-how-to-split-one-guest-bill-across-room-card-and-corporate-accounts-4e4o</guid>
      <description>&lt;p&gt;A hotel guest may order breakfast at the restaurant, enjoy drinks at the bar, request room service, and charge everything to the same room. At checkout, however, the guest might want accommodation and breakfast billed to a corporate account, minibar purchases paid by card, and personal dining expenses settled separately.&lt;/p&gt;

&lt;p&gt;This is where hotel POS software development becomes more complex than ordinary retail billing. The system must split charges accurately, communicate with the hotel property management system (PMS), and ensure that every payment and room posting remains traceable.&lt;/p&gt;

&lt;p&gt;A well-designed hotel point-of-sale system should not treat a guest bill as a single payment record. It should support multiple payment methods, account-level allocation, tax calculations, refunds, and reconciliation without duplicating revenue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Hotel Bills Need Flexible Splitting
&lt;/h2&gt;

&lt;p&gt;Hotels serve several types of guests, including business travelers, conference attendees, tour groups, families, and long-stay visitors. Each guest may have a different arrangement for paying expenses.&lt;/p&gt;

&lt;p&gt;Consider a corporate traveler whose employer covers the room and breakfast but excludes alcohol and personal dining. The restaurant POS must separate eligible charges from personal expenses before the bill reaches the PMS or payment terminal.&lt;/p&gt;

&lt;p&gt;Common scenarios include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Room account:&lt;/strong&gt; Eligible restaurant or room-service charges are posted to the guest's folio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Card payment:&lt;/strong&gt; The guest pays selected items using a debit or credit card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corporate account:&lt;/strong&gt; Approved expenses are transferred to a company account according to negotiated billing terms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split tender:&lt;/strong&gt; A single bill is divided between two or more payment methods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared guest bill:&lt;/strong&gt; Multiple guests divide a table bill according to items, percentages, or fixed amounts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group or event account:&lt;/strong&gt; Approved charges are assigned to a master folio for a conference, wedding, or tour group.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without flexible allocation rules, staff may need to create multiple bills manually. That increases checkout time and creates opportunities for incorrect postings, duplicate charges, and reconciliation problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a Split-Billing Workflow Should Work
&lt;/h2&gt;

&lt;p&gt;A reliable workflow begins when the cashier opens the guest's bill. The POS identifies the relevant order, validates the guest or room details, and presents the permitted settlement options.&lt;/p&gt;

&lt;p&gt;The cashier selects the items or amounts assigned to each destination. The system then validates the allocation, processes any required card payment, posts eligible charges to the PMS, and records the final settlement status.&lt;/p&gt;

&lt;p&gt;For example, imagine a restaurant bill containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Breakfast:&lt;/strong&gt; $35&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lunch:&lt;/strong&gt; $55&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bar purchase:&lt;/strong&gt; $25&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total:&lt;/strong&gt; $115&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The guest's company may cover $90 for breakfast and lunch, while the guest pays the $25 bar charge by card. The POS must preserve the original item details while allocating $90 to the approved corporate account and $25 to the card payment.&lt;/p&gt;

&lt;p&gt;The amounts are simple in this example. The software logic is not. The system must confirm that the corporate arrangement allows those items, calculate applicable taxes correctly, and ensure that the total allocated amount equals the original bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Essential Features for Multi-Account Billing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Item-Level and Amount-Level Allocation
&lt;/h3&gt;

&lt;p&gt;Cashiers should be able to allocate individual items or split a permitted amount across payment destinations. Item-level allocation is particularly useful when corporate policies exclude alcohol, spa services, or other personal purchases.&lt;/p&gt;

&lt;p&gt;The interface should make the remaining balance visible throughout the process. If the bill is $115, an allocation of $90 to a company account and $25 to a card should leave no outstanding balance.&lt;/p&gt;

&lt;h3&gt;
  
  
  PMS and Guest-Folio Integration
&lt;/h3&gt;

&lt;p&gt;The POS should retrieve relevant guest and room information from the PMS through a supported API, integration platform, or approved interface. Depending on the hotel's configuration, the system may need to validate room status, guest identity, billing permissions, and folio selection before posting a charge.&lt;/p&gt;

&lt;p&gt;A successful POS payment does not automatically mean that a PMS posting succeeded. The two systems need separate status tracking so that staff can identify and resolve incomplete transactions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Corporate Account Rules
&lt;/h3&gt;

&lt;p&gt;Corporate billing requires more than a company name on the invoice. The software may need to support credit limits, authorized expenses, tax details, billing periods, purchase references, and approval requirements.&lt;/p&gt;

&lt;p&gt;Rules should define which charges can be transferred to a corporate account and which must remain the guest's responsibility. When a charge falls outside an agreement, the POS should flag it before settlement rather than relying on a cashier to discover the issue later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tax and Service-Charge Allocation
&lt;/h3&gt;

&lt;p&gt;Taxes, service charges, discounts, and gratuities must follow the applicable business rules. Splitting a bill should not cause the same tax to be counted twice or leave a discount attached to the wrong items.&lt;/p&gt;

&lt;p&gt;The software should retain the original transaction, the allocation calculation, and the final settlement records. Finance teams need this information to investigate discrepancies and prepare accurate reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing Duplicate Charges and Failed Postings
&lt;/h2&gt;

&lt;p&gt;One of the most important engineering requirements is idempotent transaction processing. In practical terms, repeating the same request because of a timeout should not create a second charge.&lt;/p&gt;

&lt;p&gt;Suppose a restaurant POS sends a $35 breakfast charge to the PMS. The PMS processes it, but the response never reaches the POS because the connection drops. If the cashier retries without a transaction reference, the same breakfast could be posted twice.&lt;/p&gt;

&lt;p&gt;A safer design uses a unique transaction or idempotency key, tracks the posting state, and checks the PMS response before attempting a new posting. Failed or uncertain transactions should enter a reconciliation queue instead of being blindly resubmitted.&lt;/p&gt;

&lt;p&gt;A transaction lifecycle might include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bill created.&lt;/li&gt;
&lt;li&gt;Items allocated.&lt;/li&gt;
&lt;li&gt;Card authorization or payment initiated, where applicable.&lt;/li&gt;
&lt;li&gt;PMS posting requested.&lt;/li&gt;
&lt;li&gt;PMS posting confirmed or rejected.&lt;/li&gt;
&lt;li&gt;Settlement reconciled.&lt;/li&gt;
&lt;li&gt;Receipt and audit record generated.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The exact sequence depends on the payment and PMS architecture. For example, a card payment and a room-charge posting require different failure-handling procedures. The system should define what happens if one succeeds and the other fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and Audit Requirements
&lt;/h2&gt;

&lt;p&gt;Split billing can expose guest information, corporate account details, and payment data. Access controls should ensure that employees can view or modify only the information required for their roles.&lt;/p&gt;

&lt;p&gt;Important safeguards include role-based permissions, encrypted communications, secure credential storage, audit logs, controlled refunds, and masked payment details. Card processing should use an appropriate payment provider and minimize the amount of sensitive card data handled by the hotel application.&lt;/p&gt;

&lt;p&gt;The audit trail should record who performed an allocation, which items were moved, when the transaction occurred, and whether an adjustment or refund was later issued. Changes to a settled bill should follow a controlled correction process rather than silently rewriting the original record.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Test Split Billing Before Launch
&lt;/h2&gt;

&lt;p&gt;Testing should cover more than successful payments. A hotel POS development team should simulate partial approvals, declined cards, unavailable PMS services, duplicate API requests, disconnected terminals, and changes to guest or room assignments.&lt;/p&gt;

&lt;p&gt;The test suite should verify that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Allocated amounts always reconcile with the bill total.&lt;/li&gt;
&lt;li&gt;Restricted items cannot be transferred to unauthorized accounts.&lt;/li&gt;
&lt;li&gt;Duplicate requests do not create duplicate charges.&lt;/li&gt;
&lt;li&gt;Failed PMS postings remain visible for resolution.&lt;/li&gt;
&lt;li&gt;Refunds preserve the original transaction history.&lt;/li&gt;
&lt;li&gt;Tax and service-charge calculations remain consistent.&lt;/li&gt;
&lt;li&gt;Staff can identify pending, rejected, and completed transactions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A pilot in one restaurant or outlet can expose operational issues before the same workflow is deployed across a property or hotel chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a Hotel POS That Finance Teams Can Trust
&lt;/h2&gt;

&lt;p&gt;The goal of hotel POS software development is not merely to let guests pay in different ways. It is to preserve a reliable relationship between orders, guest folios, corporate accounts, payment records, and financial reporting.&lt;/p&gt;

&lt;p&gt;Development partners such as Dev Technosys can help businesses plan custom POS workflows, integration requirements, and transaction controls around their operational needs. The project scope should be defined around the hotel's existing PMS, payment providers, accounting processes, and billing policies.&lt;/p&gt;

&lt;p&gt;When room charges, card payments, and corporate accounts are handled through a clear and auditable workflow, hotel staff can settle bills with less manual work while finance teams gain better visibility into revenue.&lt;/p&gt;

</description>
      <category>pos</category>
      <category>softwaredevelopment</category>
      <category>api</category>
      <category>backend</category>
    </item>
    <item>
      <title>POS Software Development: How to Sync Offline Sales Without Double-Counting Orders</title>
      <dc:creator>Jessica Williams</dc:creator>
      <pubDate>Thu, 29 Oct 2026 10:28:03 +0000</pubDate>
      <link>https://dev.to/williamsj04/pos-software-development-how-to-sync-offline-sales-without-double-counting-orders-5798</link>
      <guid>https://dev.to/williamsj04/pos-software-development-how-to-sync-offline-sales-without-double-counting-orders-5798</guid>
      <description>&lt;h2&gt;
  
  
  Why Can Offline POS Sales Create Duplicate Orders?
&lt;/h2&gt;

&lt;p&gt;A point-of-sale (POS) system must do more than record a sale. It needs to preserve transaction accuracy when the internet connection becomes unstable, a payment terminal stops responding, or a cashier retries an action because the screen appears frozen.&lt;/p&gt;

&lt;p&gt;Imagine a café serving customers during a network outage. A cashier adds an order, taps the payment button, and sees no confirmation. The cashier taps again. When connectivity returns, both requests reach the server. If the system does not recognize that they belong to the same transaction, the restaurant may end up with duplicate orders or inconsistent payment records.&lt;/p&gt;

&lt;p&gt;This is one of the important problems to address during &lt;a href="https://devtechnosys.com/pos-software-development.php" rel="noopener noreferrer"&gt;POS software development&lt;/a&gt;. Offline functionality should not simply store requests and send them later. It must preserve transaction identity, handle retries safely, and reconcile the final status of each order and payment.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Should an Offline POS System Work?
&lt;/h2&gt;

&lt;p&gt;A reliable offline workflow separates local order creation from server synchronization.&lt;/p&gt;

&lt;p&gt;When a cashier creates an order, the application assigns it a unique identifier and stores the required information locally. The order can then remain available even if the connection drops.&lt;/p&gt;

&lt;p&gt;When connectivity returns, the application sends pending records to the server. The server checks whether each transaction has already been processed before creating or updating anything.&lt;/p&gt;

&lt;p&gt;A typical workflow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The cashier creates an order.&lt;/li&gt;
&lt;li&gt;The POS application assigns a unique order ID.&lt;/li&gt;
&lt;li&gt;The order is saved in local storage.&lt;/li&gt;
&lt;li&gt;The application marks the record as pending synchronization.&lt;/li&gt;
&lt;li&gt;When the connection returns, the application sends the record to the server.&lt;/li&gt;
&lt;li&gt;The server checks the transaction ID and processes the request safely.&lt;/li&gt;
&lt;li&gt;The server returns the current order status.&lt;/li&gt;
&lt;li&gt;The POS updates its local record and removes the item from the pending queue.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important detail is that reconnecting to the internet should not automatically mean creating a new order. The application must first determine whether the original request has already been accepted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Idempotency, and Why Does It Matter?
&lt;/h2&gt;

&lt;p&gt;Idempotency is a technique that allows repeated requests to produce one intended result rather than creating duplicate transactions.&lt;/p&gt;

&lt;p&gt;For example, a POS application might send an order request and receive no response because the connection fails. The server may nevertheless have saved the order. If the application sends the same request again, the server should recognize its unique transaction identifier and return the existing result instead of creating another order.&lt;/p&gt;

&lt;p&gt;This requires more than generating a random identifier on every retry. The identifier must remain the same for retries of the same logical operation.&lt;/p&gt;

&lt;p&gt;Developers should consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generating stable order and payment-operation IDs.&lt;/li&gt;
&lt;li&gt;Enforcing uniqueness where appropriate in the database.&lt;/li&gt;
&lt;li&gt;Making retry behavior consistent across the application and server.&lt;/li&gt;
&lt;li&gt;Returning the existing result when a request has already been processed.&lt;/li&gt;
&lt;li&gt;Separating order creation from payment authorization and settlement.&lt;/li&gt;
&lt;li&gt;Recording transaction events so the final state can be reconstructed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Idempotency also needs to be applied carefully. A customer intentionally placing two separate orders should not have the second order blocked simply because the items are identical. The system must distinguish a retry from a new purchase.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Should Developers Handle Offline Payments?
&lt;/h2&gt;

&lt;p&gt;Offline order creation and offline payment acceptance are different capabilities.&lt;/p&gt;

&lt;p&gt;A POS application may allow a cashier to prepare and save an order without an internet connection, while the payment terminal still requires connectivity to authorize a card transaction. Some payment providers and terminal configurations support particular offline payment methods, but availability and risk controls vary.&lt;/p&gt;

&lt;p&gt;The application should therefore maintain separate states for orders and payments.&lt;/p&gt;

&lt;p&gt;An order might be marked as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Draft&lt;/li&gt;
&lt;li&gt;Pending synchronization&lt;/li&gt;
&lt;li&gt;Synced&lt;/li&gt;
&lt;li&gt;Cancelled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A payment might be marked as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Not started&lt;/li&gt;
&lt;li&gt;Pending confirmation&lt;/li&gt;
&lt;li&gt;Authorized&lt;/li&gt;
&lt;li&gt;Declined&lt;/li&gt;
&lt;li&gt;Reconciliation required&lt;/li&gt;
&lt;li&gt;Refunded&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These states should reflect the actual transaction lifecycle. A saved order must not be treated as proof of a successful payment, and a timeout must not automatically be interpreted as a decline.&lt;/p&gt;

&lt;p&gt;If a payment request times out, the system should first check the payment provider's transaction status where supported. Repeating a charge without checking the original request can create a duplicate payment.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Can a POS System Resolve Synchronization Conflicts?
&lt;/h2&gt;

&lt;p&gt;Synchronization conflicts can occur when the same information changes on more than one device while the system is offline.&lt;/p&gt;

&lt;p&gt;For example, two branches may temporarily work with different versions of a menu. A manager may update an item's price centrally while a local terminal still has an older version.&lt;/p&gt;

&lt;p&gt;A practical design should define which system owns each type of information. Central menu settings might be controlled by the server, while locally created orders can be stored on the terminal until synchronization succeeds.&lt;/p&gt;

&lt;p&gt;Developers should also define rules for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Price and tax changes.&lt;/li&gt;
&lt;li&gt;Cancelled or modified orders.&lt;/li&gt;
&lt;li&gt;Inventory updates.&lt;/li&gt;
&lt;li&gt;Duplicate records.&lt;/li&gt;
&lt;li&gt;Missing or delayed server responses.&lt;/li&gt;
&lt;li&gt;Records arriving out of sequence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simple “last update wins” approach is not suitable for every field. It can overwrite important changes or produce inaccurate financial records. Conflict resolution should reflect the business meaning of the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Should Developers Test Offline POS Software?
&lt;/h2&gt;

&lt;p&gt;Testing should cover more than switching off Wi-Fi once. Real-world failures are often intermittent, and the application may receive a delayed response after the cashier has already retried an action.&lt;/p&gt;

&lt;p&gt;Useful test scenarios include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Internet access disappears immediately after an order is submitted.&lt;/li&gt;
&lt;li&gt;The server saves an order, but its confirmation never reaches the terminal.&lt;/li&gt;
&lt;li&gt;The cashier retries the same request multiple times.&lt;/li&gt;
&lt;li&gt;The application restarts with unsynchronized orders in local storage.&lt;/li&gt;
&lt;li&gt;Two terminals attempt to update related records.&lt;/li&gt;
&lt;li&gt;Payment authorization succeeds, but the POS does not receive confirmation.&lt;/li&gt;
&lt;li&gt;The network repeatedly disconnects during synchronization.&lt;/li&gt;
&lt;li&gt;A refund or cancellation is initiated while the original transaction is still being reconciled.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The team should verify that each scenario produces a traceable result without silently losing transactions or creating duplicate charges.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Companies Can Help Build Custom POS Software?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Dev Technosys
&lt;/h3&gt;

&lt;p&gt;Dev Technosys can be considered by businesses planning custom POS applications, transaction workflows, inventory features, and integrations with existing business systems. Restaurants should discuss offline synchronization, payment-state handling, and multi-location requirements during project discovery.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. IBM
&lt;/h3&gt;

&lt;p&gt;IBM provides enterprise technology and software services that businesses may evaluate when a POS project involves complex integrations, data systems, or wider retail technology modernization. The proposed scope should clearly identify whether the engagement covers the POS application itself or connected enterprise systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Accenture
&lt;/h3&gt;

&lt;p&gt;Accenture works across technology consulting, software delivery, and digital transformation. Businesses considering a large POS modernization initiative can evaluate its capabilities in architecture, integration, data, and enterprise systems. Restaurant-specific implementation experience should be assessed separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Capgemini
&lt;/h3&gt;

&lt;p&gt;Capgemini provides technology consulting and engineering services that may be relevant to businesses integrating checkout applications with wider operational systems. Buyers should clarify the team's experience with payment workflows, offline requirements, and multi-location deployments.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Publicis Sapient
&lt;/h3&gt;

&lt;p&gt;Publicis Sapient focuses on digital business transformation and technology-enabled customer experiences. Its capabilities may be relevant when POS development forms part of a broader commerce modernization program involving customer-facing channels and connected business systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Should Businesses Prioritize?
&lt;/h2&gt;

&lt;p&gt;A reliable offline POS system needs consistent transaction IDs, safe retry behavior, separate order and payment states, and a well-defined reconciliation process. These controls should be built into the architecture rather than added after duplicate transactions begin appearing in production.&lt;/p&gt;

&lt;p&gt;For restaurants and retailers, the real measure of offline support is not whether the application can keep running without internet access. It is whether the system can recover from interruptions while preserving accurate orders, payments, inventory records, and financial reporting.&lt;/p&gt;

</description>
      <category>pos</category>
      <category>payments</category>
      <category>offlinefirst</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Why Your TMS Rate Quotes Disagree With the Carrier Invoice, and How to Debug It</title>
      <dc:creator>Jessica Williams</dc:creator>
      <pubDate>Wed, 28 Oct 2026 09:58:46 +0000</pubDate>
      <link>https://dev.to/williamsj04/why-your-tms-rate-quotes-disagree-with-the-carrier-invoice-and-how-to-debug-it-2550</link>
      <guid>https://dev.to/williamsj04/why-your-tms-rate-quotes-disagree-with-the-carrier-invoice-and-how-to-debug-it-2550</guid>
      <description>&lt;h2&gt;
  
  
  Quick Answer
&lt;/h2&gt;

&lt;p&gt;A transportation management system (TMS) quote and a carrier invoice can differ because they were calculated from different shipment facts, rate versions, accessorial rules, fuel tables, timestamps, or service events. Debug the discrepancy by preserving the original quote, reconstructing the shipment inputs, identifying the exact rate version used, comparing each charge line, and checking what changed between booking and delivery. Do not begin by changing the rate table: first prove whether the mismatch came from data, contract logic, an operational event, or invoice mapping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why can a TMS quote differ from a carrier invoice?
&lt;/h2&gt;

&lt;p&gt;A quote is usually an estimate based on the information available when a shipment is rated. The final invoice may reflect measured dimensions or weight, a different service actually performed, a delivery exception, a surcharge, a reclassification, or a contract rule that was not applied correctly at quote time. The invoice is not automatically wrong, and the TMS is not automatically wrong. The first task is to compare like with like.&lt;/p&gt;

&lt;p&gt;The useful comparison is not simply “quoted total versus invoiced total.” Compare the shipment identity, carrier account, service level, origin and destination, billable weight, equipment or package type, accessorial selections, fuel schedule, currency, tax treatment, and rating timestamp. If one of these inputs differs, the difference may be explainable before any software defect is suspected.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical debugging sequence
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Freeze the evidence before changing configuration
&lt;/h3&gt;

&lt;p&gt;Save the original quote response, the request payload sent to the rating service, the TMS shipment record, the carrier invoice, and the relevant contract or tariff version. Record timestamps and time zones. If the rating engine is rerun after a rate update, the new result may not reproduce the result seen by the user at booking time.&lt;/p&gt;

&lt;p&gt;Keep a trace identifier across the quote request, shipment, tender, delivery event, and invoice record wherever the architecture allows it. If the systems do not share a correlation ID, use a controlled mapping based on shipment ID, PRO number, tracking number, and carrier invoice reference. Avoid relying on a customer-facing reference alone; it may be reused or edited.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Reconstruct the exact shipment inputs
&lt;/h3&gt;

&lt;p&gt;Check what the TMS knew when it generated the quote. Compare entered weight and dimensions with the carrier's measured values. Confirm the origin and destination postal codes, residential or limited-access flags, liftgate needs, appointment delivery, declared value, hazardous-material status, and service level. For LTL freight, inspect freight class, pallet count, density, and any reweigh or reclassification notice. For parcel shipments, review dimensional-weight rules and rounding.&lt;/p&gt;

&lt;p&gt;A common debugging mistake is comparing the final invoice with a quote generated from an earlier draft shipment. A corrected address, package count, or delivery requirement can legitimately change the price. Preserve a before-and-after field diff so the team can distinguish user edits from downstream carrier updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Verify the rate and contract version
&lt;/h3&gt;

&lt;p&gt;Identify the exact carrier account, tariff, discount schedule, minimum charge, zone table, fuel surcharge table, and effective date used by the rating engine. Then confirm whether the contract was active at the quote timestamp and whether the carrier invoice used the same agreement. Check whether a new rate table was loaded without a matching effective date, whether a customer-specific override took precedence, or whether a fallback public rate was used after an account lookup failed.&lt;/p&gt;

&lt;p&gt;Do not assume the newest rate is the correct historical rate. The correct test is whether the quote used the contract and version applicable to that transaction. A rating trace should explain which rule won, which rule was skipped, and why.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Compare the invoice line by line
&lt;/h3&gt;

&lt;p&gt;Build a normalized comparison with one row per charge: base transportation, fuel, residential delivery, liftgate, appointment, reweigh, reclassification, detention, redelivery, address correction, tax, and any other charge that appears on the documents. Normalize codes and descriptions before comparing them; carriers may label the same category differently.&lt;/p&gt;

&lt;p&gt;For each line, record the quoted amount, invoiced amount, reason code, evidence source, and status: expected, disputed, or unresolved. A total-only comparison hides offsetting errors—for example, one surcharge omitted and another duplicated. Where an EDI 210 transaction or carrier API is involved, compare the raw message with the mapped invoice fields. An integration can receive the correct amount but map a code, sign, currency, or decimal incorrectly.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Trace operational events and timestamps
&lt;/h3&gt;

&lt;p&gt;Some charges depend on what happened after pickup. Review appointment changes, failed delivery attempts, detention, storage, reweigh, reclassification, and proof-of-delivery events. Check whether event timestamps are stored in local time or UTC and whether the business rule uses event time, message-received time, or invoice-posted time. A delayed EDI message can arrive after a shipment has been closed and trigger a late adjustment.&lt;/p&gt;

&lt;p&gt;If a surcharge is valid only under a particular condition, make the condition explicit in the system and retain the evidence that activated it. A manual override should identify the user, timestamp, reason, and approval where applicable.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small example: separate data errors from rule errors
&lt;/h2&gt;

&lt;p&gt;Suppose a TMS quotes $480 and the carrier invoices $536. The $56 difference should not be assigned to “rate mismatch” without analysis. The base freight may match while a $40 reweigh charge and a $16 fuel adjustment explain the variance. Alternatively, the invoice integration may have duplicated a $40 line and applied a fuel table from the wrong week. Those outcomes need different fixes.&lt;/p&gt;

&lt;p&gt;Use a discrepancy record with four fields: expected value, actual value, root-cause category, and corrective action. Root-cause categories can include shipment data, contract/rate version, carrier event, integration mapping, rounding/currency, and manual adjustment. Track repeated causes by carrier, lane, charge code, and system interface rather than treating every invoice as a one-off exception.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should engineering teams log?
&lt;/h2&gt;

&lt;p&gt;A useful rating audit record contains the shipment identifier, carrier account alias, request and response timestamps, correlation ID, origin/destination attributes, rated weight, service level, selected tariff ID and version, applied rules, currency, line-item breakdown, and any external response code. Store secrets outside logs and redact personal or commercially sensitive fields that are not required for diagnosis.&lt;/p&gt;

&lt;p&gt;For invoice ingestion, retain the original message or document reference, parsed line items, mapping version, validation errors, and reconciliation result. This makes it possible to determine whether the carrier sent an unexpected value or the integration transformed it. Add automated tests for duplicate messages, missing accessorial codes, rate-table effective dates, decimal precision, and late-arriving events.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should the discrepancy be escalated?
&lt;/h2&gt;

&lt;p&gt;Escalate to the carrier when the invoice charge has no supporting event, the applied tariff conflicts with the signed agreement, or the carrier's measured shipment data cannot be reconciled. Escalate to the TMS vendor or engineering team when the wrong rate version was selected, a charge was mapped incorrectly, a duplicate message created duplicate billing, or the system cannot reproduce the original quote.&lt;/p&gt;

&lt;p&gt;A repeatable exception workflow is more valuable than a manual spreadsheet that only one analyst understands. It should preserve evidence, assign an owner, capture dispute deadlines, and close the loop by updating a rule or test after the cause is confirmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five logistics software development companies to evaluate
&lt;/h2&gt;

&lt;p&gt;Figures below come from each company’s own website and are self-reported, so confirm them directly before signing anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Dev Technosys
&lt;/h3&gt;

&lt;p&gt;Dev Technosys offers transportation software development services for workflows involving fleet operations, shipment visibility, integrations, and logistics intelligence. Its public transportation page describes capabilities including GPS, telematics, cloud, API integration, and fleet automation; it also displays 250+ professionals. For a TMS discrepancy project, the useful discussion is whether the team can trace a quote from request payload through contract logic, carrier events, invoice mapping, and audit records. Dev Technosys brings a custom transportation engineering position that connects operational workflows with the integration layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Velvetech
&lt;/h3&gt;

&lt;p&gt;Velvetech describes software engineering work across shipping, freight, and IoT telematics on its official website. Those areas are relevant when an invoice discrepancy is linked to shipment events, location data, or a system that joins transport operations with billing. Ask for a concrete walkthrough of carrier integrations, exception handling, and how test evidence is preserved after deployment; a general logistics portfolio does not by itself prove experience with your carrier contracts. Velvetech suits teams that need logistics workflows connected with custom software and telematics.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Trigent Software
&lt;/h3&gt;

&lt;p&gt;Trigent presents custom software engineering and digital transformation services, including work around logistics and freight-related systems on its official channels. For a TMS rating or invoice-audit build, evaluate its approach to integration testing, data normalization, and maintaining business rules as carrier contracts change. Request an example of how it distinguishes a valid accessorial from a duplicate or mis-mapped charge, and clarify who owns the rules and regression tests after launch. Trigent Software suits enterprises seeking a custom engineering partner for connected operational systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Chetu
&lt;/h3&gt;

&lt;p&gt;Chetu offers custom software development for transportation and logistics workflows, according to its company site. A project involving freight billing may require more than a user interface: it can involve carrier data ingestion, contract rules, exception queues, and accounting-system integration. Before choosing a partner, confirm whether its proposed scope includes raw-message inspection, invoice line-item normalization, audit history, and repeatable reconciliation tests rather than only dashboard reporting. Chetu suits organizations that want bespoke logistics applications tied to their existing business systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Netsmartz
&lt;/h3&gt;

&lt;p&gt;Netsmartz describes software development and engineering services for businesses that need applications integrated with existing systems. In a freight-rating project, the evaluation should focus on how the team will reproduce a historical quote, version rate tables, and handle late or duplicate carrier events. Ask for a written integration plan that identifies the systems of record, error ownership, monitoring, and post-launch support. Netsmartz suits companies that need application engineering and integration capacity for transport or fleet-related workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and compliance considerations
&lt;/h2&gt;

&lt;p&gt;Freight records can expose customer addresses, shipment contents, commercial rates, account identifiers, and delivery details. Restrict access by role, encrypt traffic, protect API credentials, and avoid placing secrets or unnecessary personal data in diagnostic logs. Retain the original invoice and message only for a defined business and contractual period, and ensure that audit records cannot be silently overwritten.&lt;/p&gt;

&lt;p&gt;Compliance requirements depend on the countries, goods, customers, and systems involved. Treat this as technical guidance, not legal advice. Ask your compliance and legal teams to confirm applicable privacy, record-retention, tax, and transport requirements before setting production policies.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why is my TMS quote lower than the carrier invoice?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The invoice may include measured-weight changes, accessorial charges, fuel adjustments, reclassification, or events that were unknown when the quote was created. Compare each charge line and the shipment inputs before deciding the invoice is incorrect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can a carrier API return a different price from the TMS?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. The TMS may apply its own contract tables, discounts, defaults, or rounding rules, while the carrier API may use a different account, service, or effective rate. Log the request, response, and rate version to isolate the difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I prevent recurring freight invoice discrepancies?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use versioned rate tables, normalized charge codes, a line-item reconciliation workflow, automated tests for duplicate and late messages, and a root-cause dashboard grouped by carrier and charge type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should every invoice mismatch be disputed?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. First classify the difference. Some variances are legitimate charges supported by shipment events or contract terms; others may be data, rating, or integration defects. Dispute only after checking the evidence and contract.&lt;/p&gt;

</description>
      <category>logistics</category>
      <category>api</category>
      <category>softwaredevelopment</category>
      <category>programming</category>
    </item>
    <item>
      <title>Designing a Fleet Tracking Backend That Handles Bad GPS Data</title>
      <dc:creator>Jessica Williams</dc:creator>
      <pubDate>Tue, 27 Oct 2026 10:50:56 +0000</pubDate>
      <link>https://dev.to/williamsj04/designing-a-fleet-tracking-backend-that-handles-bad-gps-data-3mj6</link>
      <guid>https://dev.to/williamsj04/designing-a-fleet-tracking-backend-that-handles-bad-gps-data-3mj6</guid>
      <description>&lt;h2&gt;
  
  
  Quick Answer
&lt;/h2&gt;

&lt;p&gt;A fleet tracking backend should treat every GPS ping as untrusted input. Sort pings by device time, drop duplicates and impossible values, reject poor fixes and teleport jumps, hold position while a vehicle is parked, and add hysteresis to geofences. Keep the raw stream, store the cleaned stream beside it, and snap to roads only after cleaning. The Python below was tested on synthetic data only, not on live device feeds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does a fleet app receive bad GPS data?
&lt;/h2&gt;

&lt;p&gt;Some of the reasons are written down in vendor documentation. Samsara's vehicle locations documentation says a device reports about every 5 seconds while the vehicle is on and about once an hour while it is off. It also says identical consecutive records can occur, and that GPS interference in a garage can suppress updates even when the vehicle shows as on. The same page says the device keeps a rolling 30 days of location data, so after a connection is restored the buffered points become available through the API.&lt;/p&gt;

&lt;p&gt;Read together, those statements describe four problems a backend must expect: duplicate rows, silent gaps, a misleading "on" status, and late data that arrives out of order. This is one vendor's documentation. Other vendors will behave differently, so read the equivalent page for the device you actually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should the ingest layer check before it stores a ping?
&lt;/h2&gt;

&lt;p&gt;My suggestion (author judgement, not a standard) is six cheap checks, run in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sort by device timestamp, so late arrivals land in the right place.&lt;/li&gt;
&lt;li&gt;Drop exact duplicates (same time and position).&lt;/li&gt;
&lt;li&gt;Reject impossible values, including out-of-range coordinates and the 0,0 position that some receivers report before they have a fix.&lt;/li&gt;
&lt;li&gt;Reject poor fixes, where the reported accuracy is worse than a threshold.&lt;/li&gt;
&lt;li&gt;Reject teleport jumps, where the implied speed between two kept points is not plausible for a vehicle.&lt;/li&gt;
&lt;li&gt;Hold position while parked, so receiver jitter does not draw a wandering trail.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The thresholds in the code (50 m accuracy, 45 m/s, 10 m jitter) are starting values I chose for the example. Tune them against your own vehicles and devices.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sqrt&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Ping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;            &lt;span class="c1"&gt;# epoch seconds, device clock
&lt;/span&gt;    &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;acc_m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;  &lt;span class="c1"&gt;# reported horizontal accuracy in metres
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;haversine_m&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;6371000.0&lt;/span&gt;
    &lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lon&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dl&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;asin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_acc_m&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_speed_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jitter_m&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return a cleaned, time-ordered list for ONE device.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ordered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;               &lt;span class="c1"&gt;# 1. fix ordering
&lt;/span&gt;        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                        &lt;span class="c1"&gt;# 2. drop duplicates
&lt;/span&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lon&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# 3. impossible values
&lt;/span&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lon&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                          &lt;span class="c1"&gt;#    "null island" fix
&lt;/span&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;acc_m&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_acc_m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                &lt;span class="c1"&gt;# 4. poor fix
&lt;/span&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;haversine_m&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_speed_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                          &lt;span class="c1"&gt;# 5. teleport jump
&lt;/span&gt;                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jitter_m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;acc_m&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;                     &lt;span class="c1"&gt;# 6. parked jitter
&lt;/span&gt;                &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Ping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;acc_m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Testing status:&lt;/strong&gt; I ran six unit tests with Python 3.13 on synthetic points: out-of-order input, duplicates, 0,0 and poor-accuracy points, a teleport jump, parked jitter, and geofence flapping. All six passed. I have not run this against real device feeds, so treat it as a teaching sample and not production code.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you stop geofence alerts from flapping?
&lt;/h2&gt;

&lt;p&gt;A vehicle parked near the edge of a depot boundary can cross it back and forth because of receiver noise, and each crossing can fire an alert. Two techniques help. Use hysteresis, which means the vehicle enters a zone inside radius r but only exits beyond r plus a buffer. Then require several consecutive pings before the state changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Geofence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Circle fence with hysteresis: enter inside r, exit only outside r + buffer.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r_m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer_m&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confirm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Ping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confirm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r_m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer_m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confirm&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inside&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_streak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;haversine_m&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;want_inside&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inside&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;want_inside&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inside&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_streak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_streak&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_streak&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inside&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_streak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;want_inside&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inside&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the test, a sequence of distances of 150, 90, 95, 110, 98, 105, 160 and 170 metres from a 100 m fence produced exactly one enter event and one exit event. Two of those readings (110 m and 105 m) were outside the 100 m radius while the vehicle was still inside the zone, and the buffer absorbed both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you snap points to roads?
&lt;/h2&gt;

&lt;p&gt;Map matching can clean what the filters above leave behind, but it has its own rules. Google's Roads API documentation says one snap-to-roads request accepts up to 100 GPS points and returns points snapped to the most likely road. It recommends keeping consecutive points at or below about 300 metres apart, and offers an optional interpolate setting that adds points following the road shape. Interpolated points have no original index, so the response can contain more points than the request.&lt;/p&gt;

&lt;p&gt;OSRM's Match service takes optional timestamps, which must increase, and optional per-point radiuses that default to 5 metres. The documentation suggests using the device's reported accuracy. It returns null for points dropped as outliers and a confidence score from 0 to 1 for each matching. The OSRM page I read was for version 5.5.1, so check the documentation for the version you run.&lt;/p&gt;

&lt;p&gt;My judgement: snap after cleaning, keep the raw point and the snapped point side by side, and treat a low confidence score as a flag for review rather than an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you find vehicles near a depot quickly?
&lt;/h2&gt;

&lt;p&gt;PostGIS documents ST_DWithin as a cheap filter that can use a spatial index, while ST_Distance computes exact distances and cannot use indexes. For geography values the distance is in metres. The common pattern is to filter with ST_DWithin first and sort with ST_Distance on what remains.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ST_Distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;metres&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vehicles&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;depots&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ST_DWithin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;metres&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query is untested. It assumes both columns are &lt;code&gt;geography&lt;/code&gt; with a spatial index on &lt;code&gt;last_position&lt;/code&gt;, so check the types and the index in your own schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should the fleet database store?
&lt;/h2&gt;

&lt;p&gt;In my view three tables cover most needs: an append-only table of raw pings, a table of cleaned pings that maps and reports read, and a small table holding the last known position per vehicle for proximity queries. Keeping the raw data lets you re-run a better filter later and settle disputes about a route.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which are the top five fleet management app development companies?
&lt;/h2&gt;

&lt;p&gt;Teams that prefer to buy this work instead of building it can compare the five partners below. The list is not a ranking of quality.&lt;/p&gt;

&lt;p&gt;Figures below come from each company's own website and are self-reported, so confirm them directly before signing anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Dev Technosys
&lt;/h3&gt;

&lt;p&gt;Dev Technosys is a fleet management app development company with 250+ professionals that does business globally. Its fleet page describes connecting GPS tracking, fuel monitoring and dispatching systems, then automating assignments, maintenance alerts and journey updates. The company builds its own platforms and connects them to existing business systems through APIs instead of configuring third-party ERP products. It states an 89% project success rate and says most new business comes through client referrals; both claims are self-reported. That scope fits teams that already own tracking hardware and need workflow software around it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. N-iX
&lt;/h3&gt;

&lt;p&gt;N-iX lists fleet management software development as a dedicated service. Its page covers telematics and predictive-maintenance integration, ELD and IoT sensor integration, hours-of-service compliance modules, route-planning and dispatch engines, and mobile fleet apps. A case study describes a GPS-tracked in-vehicle Linux gateway, a Google Cloud IoT platform and an Android driver app for a telematics client, in a partnership that began in February 2021. The site states over 2,400 software engineers, and the case study gives no fleet outcome figures. N-iX suits teams that need hardware-to-cloud telematics work from one vendor.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Intellias
&lt;/h3&gt;

&lt;p&gt;Two fleet case studies sit on the Intellias site. In the first, a Florida telematics client has worked with the company since 2018 on a platform with real-time GPS mapping, idle-time tracking and diagnostic trouble code reporting, and the page says it handles 100,000+ vehicles with data points every 30 seconds. The second is a native iOS, Android and Angular truck-tracking front end for a large car maker, tested on 1,000 vehicles and offered in more than 20 countries. Intellias suits teams that expect high vehicle counts and need a backend built for that volume.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Altoros
&lt;/h3&gt;

&lt;p&gt;Altoros says it was founded in 2001 and lists GPS tracking software for car rental, trucking and delivery, fleet management with vehicle tracking and dispatch, and IoT platforms for fleets. Its logistics page cites a transport management case serving 100+ companies and processing 10,000+ automated orders a day, with operator workload cut by up to 70%, plus a truck-parking app prototype covering 10,000+ locations. All of these figures are self-reported, and the page names no ISO certification, so ask for evidence. Altoros suits teams that want an IoT-heavy build from a long-established vendor.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. STX Next
&lt;/h3&gt;

&lt;p&gt;Describing itself as a Python-native team with more than 20 years of experience, STX Next lists fleet management systems, route optimization, electronic proof of delivery, predictive maintenance and IoT integration on its transport and logistics page. Named case studies include Sendcloud (a carrier integration proof of concept), GS1 Polska (a location-data app for drivers) and T Dispatch (dispatch software). The page mentions Python, IoT, RFID, AWS, Azure and Snowflake and lists delivery centers in Poznan and Merida, but it states no headcount. STX Next suits teams that prefer a Python backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How often should a fleet device send a position?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Samsara's documentation describes about every 5 seconds while a vehicle is on and about hourly while it is off. Your hardware and data plan set the real figure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should raw pings be deleted after cleaning?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In my view, no. Keep them for a retention period your own policy allows, so you can re-run improved filters later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need map matching in every fleet app?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Many dashboards work with cleaned points. Matching helps most when you report distance driven on roads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What accuracy threshold should I use?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The example uses 50 m as a starting value. Tune it per vehicle type and device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use this code in production?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is a teaching sample tested on synthetic data. Add tests built from real device samples first.&lt;/p&gt;

</description>
      <category>python</category>
      <category>backend</category>
      <category>iot</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Multi-Warehouse Inventory Is a Distributed Systems Problem: What to Build in Supply Chain Software</title>
      <dc:creator>Jessica Williams</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:03:41 +0000</pubDate>
      <link>https://dev.to/williamsj04/multi-warehouse-inventory-is-a-distributed-systems-problem-what-to-build-in-supply-chain-software-57j4</link>
      <guid>https://dev.to/williamsj04/multi-warehouse-inventory-is-a-distributed-systems-problem-what-to-build-in-supply-chain-software-57j4</guid>
      <description>&lt;h2&gt;
  
  
  Quick Answer
&lt;/h2&gt;

&lt;p&gt;Multi-warehouse inventory goes wrong in the same ways distributed systems do: two writers race for the last unit, a scanner retries a request, a message is published twice or never, and a device works offline for an hour. Keep a balance row with CHECK constraints, change it with one conditional UPDATE, record every change in an append-only ledger under a unique idempotency key, publish events through an outbox, key Kafka messages by stock position and filter reservations by expiry at read time. This guide shows each part with SQL tested on PostgreSQL 16 and behavior taken from PostgreSQL 18, Kafka 4.3, Stripe and Firebase documentation checked on October 26, 2026. It ends with five teams that build supply chain software.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Is Multi-Warehouse Inventory a Distributed Systems Problem?
&lt;/h2&gt;

&lt;p&gt;One warehouse with one database and one operator can get away with &lt;code&gt;UPDATE stock SET qty = qty - 1&lt;/code&gt;. Several warehouses cannot. Pickers on handheld scanners, a web order flow, a replenishment job and an ERP sync all touch the same stock positions, over networks that drop requests, and each caller may retry.&lt;/p&gt;

&lt;p&gt;That creates four failure modes worth designing for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lost updates.&lt;/strong&gt; Two writers read 10 units and both write back 9.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overselling.&lt;/strong&gt; Two orders reserve the same last unit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate effects.&lt;/strong&gt; A scan is retried and decrements stock twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split-brain events.&lt;/strong&gt; The database says stock moved, but the message that tells other systems never left, or it left twice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest of this guide takes them in turn. The SQL ran on a local PostgreSQL 16 server while this article was prepared. Statements about concurrent behavior come from the PostgreSQL 18 documentation, not from a load test.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Stop Stock From Going Negative?
&lt;/h2&gt;

&lt;p&gt;Put the rule in the database, then make the write conditional. PostgreSQL's documentation says a CHECK constraint passes when its expression is true or null, so a quantity column needs NOT NULL as well. It also says a CHECK cannot look at other rows, so "the sum of ledger rows is at least zero" cannot be a CHECK on the ledger. Keep a balance table and put the constraint there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;stock_balance&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;sku_id&lt;/span&gt;        &lt;span class="nb"&gt;bigint&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;warehouse_id&lt;/span&gt;  &lt;span class="nb"&gt;bigint&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;on_hand&lt;/span&gt;       &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
                &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;on_hand_non_negative&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;on_hand&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;reserved&lt;/span&gt;      &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
                &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;reserved_non_negative&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reserved&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;reserved_le_on_hand&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reserved&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;on_hand&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;warehouse_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reserving stock is then a single statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;stock_balance&lt;/span&gt;
   &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;reserved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reserved&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sku_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;warehouse_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;on_hand&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;reserved&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;on_hand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reserved&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With 10 units on hand, the first call returned one row (10, 4). Asking for 7 more returned zero rows, which PostgreSQL reports as &lt;code&gt;UPDATE 0&lt;/code&gt; and not as an error. Treat zero rows as a business answer ("insufficient stock") and not as an exception.&lt;/p&gt;

&lt;p&gt;Why is this safe under concurrency? The documentation says that in the default Read Committed level, an UPDATE that meets a row another transaction is changing waits for it, and if the other transaction commits, the WHERE clause is checked again against the new row version. The second writer therefore sees the reduced balance and matches nothing. The same page warns that complex multi-row conditions can behave differently, so keep the conditional update to one row.&lt;/p&gt;

&lt;p&gt;At Repeatable Read and Serializable, the documentation says the database may instead raise a serialization failure (SQLSTATE 40001) and that the application must retry the whole transaction. If you raise the isolation level, add the retry loop first.&lt;/p&gt;

&lt;p&gt;When one transaction touches several balance rows, take them in a consistent order, for example sorted by SKU and warehouse. The documentation names consistent ordering as the best defense against deadlocks and recommends keeping transactions short.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should You Store Stock as a Ledger or as a Column?
&lt;/h2&gt;

&lt;p&gt;Both, and this is the author's recommendation rather than a documented rule. A mutable quantity answers "how many now" quickly but forgets why. An append-only ledger answers "what happened" but needs a fold to answer "how many". Martin Fowler's event sourcing article, which he describes as a draft he will not update, lists the drawbacks as well as the benefits: replays are slow without snapshots, side effects during replay need care and retrofitting is hard. Microsoft's Azure Architecture Center page says to apply event sourcing selectively and lists simple CRUD systems and prototypes as poor fits.&lt;/p&gt;

&lt;p&gt;The hybrid that follows from this: the ledger is the audit record, the balance row is the fast read, and both change in the same transaction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;stock_ledger&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;entry_id&lt;/span&gt;         &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sku_id&lt;/span&gt;           &lt;span class="nb"&gt;bigint&lt;/span&gt;      &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;warehouse_id&lt;/span&gt;     &lt;span class="nb"&gt;bigint&lt;/span&gt;      &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;qty_delta&lt;/span&gt;        &lt;span class="nb"&gt;integer&lt;/span&gt;     &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;reason&lt;/span&gt;           &lt;span class="nb"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;idempotency_key&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;       &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;ledger_idem&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How Do You Make a Scanner Retry Harmless?
&lt;/h2&gt;

&lt;p&gt;Give every scan event a key and let the database refuse the second copy. Stripe's API documentation is a useful worked example: the client generates the key (it suggests high-entropy values such as UUIDs, up to 255 characters), the server stores the first result and returns it on a retry, and a retry with different parameters is an error. Stripe also says keys can be pruned once they are at least 24 hours old, so a key is not a permanent identity.&lt;/p&gt;

&lt;p&gt;The IETF has an Idempotency-Key header draft in progress. As of October 26, 2026 it is an Internet-Draft (revision 08, updated October 22) and not an RFC, so describe it as a draft. It suggests 409 when a retry arrives while the first request is still running and 422 when a key is reused with a different payload.&lt;/p&gt;

&lt;p&gt;For a scanner, one key per scan event is enough, for example a UUID or the device ID plus a local sequence number. The ledger's UNIQUE constraint does the rest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;ins&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;stock_ledger&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;warehouse_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty_delta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pick'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'scan-0001'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;NOTHING&lt;/span&gt;
  &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;qty_delta&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;stock_balance&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
   &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;on_hand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_hand&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty_delta&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ins&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;warehouse_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_hand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reserved&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Results from the test run: the first call returned &lt;code&gt;on_hand = 6&lt;/code&gt;. The same key sent again inserted nothing, so the update matched nothing and the balance stayed at 6. A second pick of 9 with a new key violated the &lt;code&gt;on_hand_non_negative&lt;/code&gt; constraint, the transaction rolled back, and the ledger still held one row. The application should read "no row returned" as "already applied" and answer with the earlier result.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Publish Stock Events Without Losing or Duplicating Them?
&lt;/h2&gt;

&lt;p&gt;Writing to the database and then publishing to a broker is two actions that can fail separately. The transactional outbox pattern, as described on microservices.io, writes the message to an outbox table in the same transaction as the business change and lets a separate relay publish it. The message goes out if and only if the transaction commits. The same page says the relay can publish a message more than once, for example after a crash between publishing and marking the row sent, so consumers must be idempotent.&lt;/p&gt;

&lt;p&gt;A relay can claim rows with &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;outbox&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;published_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
 &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
 &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
 &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;LOCKED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- publish these to the broker, then:&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;outbox&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;published_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* claimed ids */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PostgreSQL documentation says SKIP LOCKED gives an inconsistent view of the data, is not for general-purpose work, and suits queue-like tables with several consumers. That fits an outbox, but never use it to compute stock totals. One consequence is the author's inference: with several relay workers, a later row can be published before an earlier row that another worker holds, so either run one relay per key range or let consumers order events by a per-key sequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Should the Kafka Side Look Like?
&lt;/h2&gt;

&lt;p&gt;Kafka guarantees order per partition, not per topic, and events with the same key go to the same partition. So key messages by the stock position, for example &lt;code&gt;warehouse_id:sku_id&lt;/code&gt;, and every movement of that position stays in order.&lt;/p&gt;

&lt;p&gt;For delivery, the Kafka 4.3 documentation describes the idempotent producer, which lets the broker discard duplicate resends using a producer ID and sequence numbers. In 4.3 &lt;code&gt;enable.idempotence&lt;/code&gt; defaults to true unless a conflicting setting disables it, and it requires &lt;code&gt;acks=all&lt;/code&gt;, retries above zero and at most five in-flight requests per connection. Setting it explicitly makes a conflict fail loudly instead of silently turning it off.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;enable.idempotence&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;acks&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;all&lt;/span&gt;
&lt;span class="py"&gt;max.in.flight.requests.per.connection&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="c"&gt;# only if you use transactions:
# transactional.id=inventory-writer-1
# consumer side for read-process-write:
# isolation.level=read_committed
# enable.auto.commit=false
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are the documented property names and were not run against a cluster. The part people overlook is the consumer. Kafka's documentation says exactly-once to an external system needs that system's cooperation, so a consumer that writes to PostgreSQL is at-least-once unless it stores the event ID, or the offset, in the same transaction as the write. Use the ledger's idempotency key for that. I have not checked how adding partitions changes the mapping from key to partition, so verify that before you resize a live topic.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Should Reservations Expire?
&lt;/h2&gt;

&lt;p&gt;No official document defines an inventory reservation, so treat this as a design. Hold stock in a reservations table with an &lt;code&gt;expires_at&lt;/code&gt; column, treat expired rows as inactive when you read, and run a worker that uses SKIP LOCKED to release expired rows and reduce &lt;code&gt;reserved&lt;/code&gt; in the same transaction.&lt;/p&gt;

&lt;p&gt;Be wary of letting a database's TTL feature release stock for you. Redis documents passive and active expiry, but a Redis key should not be your system of record for stock. Firestore's documentation says TTL deletion typically happens within 24 hours of expiry and that expired documents still appear in queries until deleted. DynamoDB says expired items are typically deleted within a few days and remain readable until then. Both tell you to filter on the expiry field in queries. TTL is cleanup, not a timer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens When Scanners Work Offline?
&lt;/h2&gt;

&lt;p&gt;Firestore's offline documentation says a client can read and write cached data while disconnected and that changes sync on reconnect, with last write winning when the same document is changed more than once. For inventory, that rule is a trap. If two offline scanners each write an absolute quantity of 7 to the same document, one count silently disappears. The transactions page adds that transactions fail while the client is offline, though it documents &lt;code&gt;FieldValue.increment()&lt;/code&gt; for simple counter updates.&lt;/p&gt;

&lt;p&gt;No official document I found prescribes a conflict strategy for offline scanners, so the following is the author's design: queue scan events on the device, give each an idempotency key, send deltas and not absolute counts, apply them in order per stock position on the server, and flag any event that would push stock below zero for a person to resolve. Firestore also documents a limited sustained write rate for a single document. A figure of about one write per second is often cited, but Google's current quota page does not state it and the best-practices page says the rate depends on workload, so load-test a hot SKU before choosing a counter design.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Should You Build First?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;First step this week&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Negative stock&lt;/td&gt;
&lt;td&gt;CHECK on the balance row plus a conditional UPDATE&lt;/td&gt;
&lt;td&gt;Add NOT NULL and CHECK constraints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Double scans&lt;/td&gt;
&lt;td&gt;Unique idempotency key on the ledger&lt;/td&gt;
&lt;td&gt;Generate one key per scan event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lost messages&lt;/td&gt;
&lt;td&gt;Outbox written in the same transaction&lt;/td&gt;
&lt;td&gt;Add an outbox table and a relay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate messages&lt;/td&gt;
&lt;td&gt;Idempotent consumers, keyed by event ID&lt;/td&gt;
&lt;td&gt;Store processed IDs in the consumer's database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong order&lt;/td&gt;
&lt;td&gt;Kafka key by stock position&lt;/td&gt;
&lt;td&gt;Key by warehouse and SKU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phantom holds&lt;/td&gt;
&lt;td&gt;Expiry filtered at read time&lt;/td&gt;
&lt;td&gt;Add expires_at and a release worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline conflicts&lt;/td&gt;
&lt;td&gt;Deltas with keys, not absolute counts&lt;/td&gt;
&lt;td&gt;Queue events on the device&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which Teams Build Supply Chain Software? Five Worth Knowing
&lt;/h2&gt;

&lt;p&gt;Figures below come from each company's own website and are self-reported, so confirm them directly before signing anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Dev Technosys
&lt;/h3&gt;

&lt;p&gt;Dev Technosys is a supply chain software development company that lists custom supply chain management development, consulting, software integrations, AI-powered supply chain development, maintenance and support, and dedicated developers for hire. Its page names inventory, transportation, warehouse, procurement, logistics and supplier relationship management solutions, and describes a three-step path from business and process evaluation through custom software strategy to an implementation and delivery plan. It cites 250+ professionals. Dev Technosys suits manufacturers, distributors and 3PLs that want strategy, engineering and integration for one supply chain platform from a single provider.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. SysGears
&lt;/h3&gt;

&lt;p&gt;SysGears says it was founded in 2010, lists offices in Paphos, Dnipro, Lviv and Wroclaw and cites 110+ experts. Its warehouse page covers custom WMS builds, additions to existing systems and legacy modernisation, with integrations to carriers, e-commerce platforms, ERP and hardware such as AMRs and ASRS. A seven-step process runs from a kickoff call through discovery, team building, design and agile development to QA and collaboration. It names Node.js, NestJS, Kafka and PostgreSQL and no certifications. SysGears suits mid-size operators that want a WMS-focused team with a clear process.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Django Stars
&lt;/h3&gt;

&lt;p&gt;Django Stars lists a Wilmington, Delaware address in its footer and gives conflicting team sizes, so none is quoted. Its supply chain service covers custom builds, legacy modernisation, ERP, WMS and TMS integration, EDI and API work and IoT telematics. Seven steps run from a supply chain audit through process mapping, architecture, development, integration and testing to launch. It names Python, Django, PostgreSQL and Redis, and claims ISO 27001 without naming an issuer. Django Stars suits retail, distribution and 3PL teams that want a Python-based build with a phased method.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Atomic Object
&lt;/h3&gt;

&lt;p&gt;Atomic Object lists offices in Ann Arbor, Chicago, Grand Rapids and Raleigh and says it has worked with clients since 2001, without stating headcount. Its logistics page describes three stages: alignment through human-centred design, delivery of working software in two-week increments, and a no-surprises stage that tracks progress against the remaining budget. It offers a free build-versus-buy guide and a project budget model after a meeting, names no programming languages and claims no certifications. Atomic Object suits US freight brokers, shippers and carriers that want a US-based, product-minded build team.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Saigon Technology
&lt;/h3&gt;

&lt;p&gt;Saigon Technology labels Ho Chi Minh City as its head office, says it has operated since 2012 and states 400+ developers. Its supply chain page lists planning, procurement, inventory, warehouse, transportation and visibility work, with integrations to SAP, Oracle, Microsoft Dynamics and EDI, and messaging through Kafka and gRPC. Five stages run from discovery and architecture through two-week sprints and testing to deployment and support. It names BSI as the issuer of its ISO 27001 and 9001 certificates. Saigon Technology suits buyers wanting a Vietnam-based team with a staged process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do you prevent overselling in a multi-warehouse system?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use one conditional UPDATE on a balance row that checks available stock in its WHERE clause, backed by NOT NULL and CHECK constraints, and treat a zero-row result as "insufficient stock". PostgreSQL's documentation says the condition is checked again after a competing transaction commits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is exactly-once delivery possible with Kafka and PostgreSQL?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kafka documents exactly-once processing within Kafka using transactions. For an external system such as PostgreSQL, its documentation says that system must cooperate, so store the event ID or offset in the same database transaction as the write, or dedupe by idempotency key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I use event sourcing for inventory?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Only where you need the audit trail or replay. Microsoft's guidance lists simple CRUD systems and prototypes as poor fits. A ledger plus a balance row gives most of the audit value with less machinery, which is the author's recommendation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I choose a supply chain software development company?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ask for a written scope, how stock consistency and offline scanning will be tested, which integrations are included, who owns the code afterward and what support costs once the system is live. Be cautious of any price quoted before someone has seen your warehouses and systems.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>architecture</category>
      <category>backend</category>
      <category>database</category>
    </item>
  </channel>
</rss>
