"<h1>Changelog<\/h1>\n<h2>2026-07<\/h2>\n<h3>Week 1 \u00b7 Jul 1+<\/h3>\n<ul>\n<li><strong>MOD<\/strong> Brand tracker pages are now cleaner hubs: thin or duplicate model listings are consolidated into a single page while older links keep working, so each brand reads as one clear directory instead of scattered near-duplicates.<\/li>\n<li><strong>MOD<\/strong> Refreshed the device pages for popular Coban, Concox and Teltonika models with tighter, more honest descriptions and setup answers.<\/li>\n<\/ul>\n<h2>2026-06<\/h2>\n<h3>Week 5 \u00b7 Jun 29-30<\/h3>\n<ul>\n<li><strong>ADD<\/strong> Comparison pages now have shorter, stable URLs, and older comparison links still send visitors to the right page.<\/li>\n<li><strong>ADD<\/strong> Alternatives pages now have shorter, stable URLs, and older alternatives links still send visitors to the right page.<\/li>\n<li><strong>MOD<\/strong> Comparison pages now show the current Traxelio price automatically when public pricing changes.<\/li>\n<li><strong>MOD<\/strong> Industry pages now live under clearer use-case URLs, with old industry and GPS category links redirected so saved links and search traffic keep working.<\/li>\n<\/ul>\n<h3>Week 4 \u00b7 Jun 22+<\/h3>\n<ul>\n<li><strong>ADD<\/strong> Traxelio now watches for a frozen GPS signal: when your tracker stays online but its position stops moving while the vehicle is clearly driving, you get a safety alert by email, push, and WhatsApp so the pin on the map can't quietly lie about where your vehicle is. The alert comes with a one-tap booking to schedule a maintenance visit and get the tracker checked, and it clears itself automatically as soon as the position starts moving again.<\/li>\n<\/ul>\n<h2>2026-05<\/h2>\n<h3>Week 5 \u00b7 May 25+<\/h3>\n<ul>\n<li><strong>ADD<\/strong> High-intent leads now receive a personalised five-step email nurture written by AI: an initial value email on day 0, a social proof follow-up on day 2, an objection-handling message on day 4, an urgency nudge with a mobile push on day 8, and a break-up message with a final push on day 13. The sequence cancels automatically when the lead purchases. Operators can enrol or delist any lead from any active sequence directly in the admin panel.<\/li>\n<\/ul>\n<h3>Week 1 \u00b7 May 4+<\/h3>\n<ul>\n<li><strong>FIX<\/strong> Tracker-offline email now renders fully in French: the &quot;what you can do now&quot; steps, the &quot;Check device status&quot; CTA, and the appointment detail labels were falling back to English because of stale translation keys.<\/li>\n<li><strong>MOD<\/strong> Tracker-offline email headline now reads &quot;Votre traceur HOWO 1 AB-760-HW ne transmet plus. Dernier contact : ...&quot; \u2014 tighter, scannable, and on-voice.<\/li>\n<li><strong>MOD<\/strong> Site-wide brand color refresh: every page that previously rendered the legacy pink-orange gradient now uses the locked Luxury Purple + Antique Gold palette. Hero gradients, CTAs, badges, and accents are consistent across landing, comparisons, marketplace, learn, faq, pricing, tools, industries, trackers, and email templates.<\/li>\n<li><strong>FIX<\/strong> Comparison pages now show $29\/vehicle\/month consistently across the hero, stats bar, calculator, FAQ, and CTA. The &quot;$20&quot; placeholder copy from earlier drafts has been removed everywhere.<\/li>\n<li><strong>MOD<\/strong> Comparison-page Geotab card now matches the Traxelio card layout: 4 tiles in a 2\u00d72 grid (Monthly price \u00b7 Contract \u00b7 Hardware \u00b7 Best for), with hardware costs surfaced as their own tile rather than crammed into the pricing string.<\/li>\n<li><strong>MOD<\/strong> Comparison-page strengths-vs-weaknesses columns now have equal counts on every comparison (4 vs 4 by default), so the side-by-side cards align cleanly. Weakness sentences are real, specific, and competitor-by-competitor.<\/li>\n<li><strong>MOD<\/strong> &quot;More Comparisons&quot; related-card pricing strings now follow a single canonical mask (<code>$X-$Y\/vehicle\/month<\/code> or <code>Quote-based<\/code>), so the cards read consistently regardless of which competitor you land on.<\/li>\n<li><strong>MOD<\/strong> Comparison-page &quot;View all comparisons&quot; upgraded from a small text link to an outlined button.<\/li>\n<li><strong>ADD<\/strong> Comparison page now shows a real headshot of the ABI Research analyst quoted on the page (sourced from their official analyst bio), making the third-party citation feel like a real person rather than initials in a circle.<\/li>\n<li><strong>ADD<\/strong> Every comparison page (24 in total) now has a full French translation of the body content (subtitle, narrative, advantages, weaknesses, FAQs, support table). Locale switch is instant.<\/li>\n<li><strong>ADD<\/strong> Decision Guide on the comparison pages now offers a third option: &quot;Still on the fence&quot; \u2014 run both stacks side by side via the API while you decide. Talks to a human via the contact page.<\/li>\n<li><strong>ADD<\/strong> Industry-expert quotes now show a name-initial avatar and link to the source publication, lifting credibility next to each verbatim quote. The North America quote on the landing page is now a hardware-neutral line from a DAT Freight &amp; Analytics analyst (the Geotab-specific ABI quote stays on the comparison page where it belongs).<\/li>\n<li><strong>MOD<\/strong> Pricing-transparency feature row gained a help icon with a hover tooltip explaining what &quot;Public&quot; vs &quot;Reseller-quoted&quot; means in practice.<\/li>\n<li><strong>MOD<\/strong> Migration step &quot;Check hardware compatibility&quot; now states explicitly that Traxelio-supplied trackers start at $30, one-time, no monthly markup \u2014 instead of the ambiguous &quot;alternatives start at $30&quot;.<\/li>\n<li><strong>MOD<\/strong> &quot;Updated &lt;month&gt;&quot; badge on every comparison page is now driven by the page's actual last-edit timestamp, so the date stays honest as content changes.<\/li>\n<li><strong>ADD<\/strong> Comparison and landing pages now carry an &quot;Industry says&quot; section with three sourced quotes from named analysts (ABI Research, Berg Insight, IRU) framing the fleet-tech market. Quotes link out to the original publications.<\/li>\n<li><strong>ADD<\/strong> Geotab comparison page now shows a 3-logo trust strip (&quot;Trusted by fleets across West Africa&quot;) and a mobile-first product block with the live-map screenshot, owning the mobile-only product reality as a positioning choice.<\/li>\n<li><strong>ADD<\/strong> French body translation for the Geotab comparison page (subtitle, narrative, advantages, weaknesses, FAQs, support table). Other 23 comparisons remain English-only for now.<\/li>\n<li><strong>MOD<\/strong> Geotab comparison FAQ expanded from 3 to 8 questions covering pricing math, GO9 hardware portability, migration timelines, contract penalties, ELD\/HOS support, and support SLA \u2014 the questions a real Geotab evaluator searches before switching.<\/li>\n<li><strong>MOD<\/strong> French translation of every UI string on the comparison-page template (section headings, calculator, timeline, decision guide, sticky sub-nav, CTAs).<\/li>\n<li><strong>FIX<\/strong> Comparison pages (e.g. \/compare\/traxelio-vs-geotab) now show the correct cost in the pricing calculator. Previously the slider rendered Traxelio at 100\u00d7 the right amount (e.g. $29,000\/month for 10 vehicles instead of $290), which made every comparison look wildly more expensive than the competitor.<\/li>\n<li><strong>FIX<\/strong> Comparison page implementation timelines no longer overlap on weeks 2, 4, and 8. Competitor onboarding phases now read as clean, non-overlapping ranges.<\/li>\n<li><strong>MOD<\/strong> Comparison page redesign pass: hero now has a primary &quot;Start free&quot; CTA, an in-page sticky sub-nav (Pricing \u00b7 Features \u00b7 Support \u00b7 Switching \u00b7 FAQ), unified CTA labels, brand-token colors throughout (no more amber or orange chips), and a balanced head-to-head card grid.<\/li>\n<li><strong>MOD<\/strong> Comparison page sources block dropped the misleading &quot;(as of 2026-05-04)&quot; suffix on every link; the global &quot;as of February 2026&quot; disclaimer remains as the single freshness signal.<\/li>\n<\/ul>\n<h2>2026-03<\/h2>\n<h3>Week 9 \u00b7 Apr 26+<\/h3>\n<ul>\n<li><strong>FIX<\/strong> Fuel reports now show correct min, max, and average readings. The previous SQL aggregate compared values as text, so a tank reading of 9.8 L could sort above 10.1 L and the summary could report an average higher than the maximum.<\/li>\n<li><strong>FIX<\/strong> Order, payment, and device-transaction pages now show correct totals after the XOF cents migration. Previously, completed XOF payments coming in from Wave and Orange Money were stored in face-value units while order items were in cents, which made order pages display 100\u00d7 inflated totals and broke the &quot;paid in full&quot; check on already-paid orders.<\/li>\n<li><strong>FIX<\/strong> Orders created manually from the admin panel no longer enroll customers into the abandoned cart follow-up sequence (emails and WhatsApp). Only customer-driven checkouts trigger that funnel now.<\/li>\n<li><strong>ADD<\/strong> New blog post: &quot;Poss\u00e9der un taxi \u00e0 Dakar : ce que personne ne te dit sur le revenu r\u00e9el&quot; \u2014 covers the 170,000 FCFA\/month gap between theoretical and actual taxi owner income, how information asymmetry drives it, and how GPS closes it. Available in French and English.<\/li>\n<li><strong>ADD<\/strong> Two new blog posts: &quot;Combien d'argent tu perds sans GPS sur ta flotte ?&quot; (fleet ROI breakdown with fuel fraud, idle time, and route deviation savings) and &quot;Traxelio vs concurrents GPS au S\u00e9n\u00e9gal&quot; (honest comparison table across 6 criteria). Both available in French and English.<\/li>\n<li><strong>ADD<\/strong> Fleet trip detail view for dashcam operators: frame-accurate timeline player with speed, incident markers, and clip timestamps so reviewers jump straight to events without scrubbing the full recording.<\/li>\n<li><strong>ADD<\/strong> Live dispatch board for transport operators: WebSocket-powered screen showing each vehicle's current passenger count, updated automatically as the camera counts heads. No page refresh needed.<\/li>\n<li><strong>ADD<\/strong> Driver portal: drivers can now log in and see their own trip history and trip detail. Useful for dispute resolution and performance coaching without granting full fleet admin access.<\/li>\n<li><strong>ADD<\/strong> Passenger footage portal: at the end of a trip, the passenger automatically receives a signed link by email. They can view the trip summary and submit a clip request if they need footage for an insurance claim or complaint.<\/li>\n<li><strong>ADD<\/strong> Public acquisition page at \/acquire with a gated two-page brief download. Includes live product metrics, a build timeline, and a competitive comparison. Any potential acquirer or strategic partner can request the brief with name, email, company, and role.<\/li>\n<li><strong>ADD<\/strong> Fleet owners now receive an email alert when an unrecognized driver is detected on their camera, with one-click links to confirm or flag the driver \u2014 no login required.<\/li>\n<li><strong>ADD<\/strong> Camera alerts (fatigue, incident) are now sent by email in addition to push notification, and only when the account has an email address on file.<\/li>\n<li><strong>FIX<\/strong> Cart total now returns a formatted price string in the API response so the mobile cart screen displays the correct currency format.<\/li>\n<\/ul>\n<h3>Week 8 \u00b7 Apr 19-25<\/h3>\n<ul>\n<li><strong>ADD<\/strong> Enterprise plan now includes a one-time onboarding fee for API-only integrations (webhook endpoint, data mapping, UAT, SLA). New FAQ entries explain enterprise pricing, API availability, and hardware sourcing. Visible on the pricing page.<\/li>\n<li><strong>ADD<\/strong> All product images are now automatically watermarked with a tiled logo pattern at conversion time. New images are protected on creation with no manual step.<\/li>\n<li><strong>ADD<\/strong> When a user deletes their own account, they automatically receive a short personal email from the founder asking what went wrong. The email adapts to three exit profiles: left within 24 hours, left without ever buying, or left after purchase.<\/li>\n<li><strong>FIX<\/strong> Store dropdown now shows the correct store pre-selected when editing an existing product order item, so the product list loads immediately instead of appearing blank.<\/li>\n<li><strong>ADD<\/strong> Comparison pages for 13 more GPS tracking competitors: Samsara, Verizon Connect, Azuga, GPS Trackit, Teletrac Navman, Quartix, 3DTechLogis, GPS My Tracker, Geo-Tracking, Natyatrack, IT Mobile Afrique, Tracking Africa, and Tramigo, each with full pricing and feature breakdowns.<\/li>\n<li><strong>FIX<\/strong> Checkout abandonment follow-up emails now correctly link back to your cart (with store pre-selected when available), restoring the CTA that was silently broken.<\/li>\n<li><strong>MOD<\/strong> Alert notifications now use clearer, more direct copy: vehicle name leads, times are shown at a glance, and filler lines (&quot;This is to inform you...&quot;, &quot;Please contact support&quot;) are gone.<\/li>\n<li><strong>FIX<\/strong> Device alert emails (curfew violation, speed limit, geofence, towing) are now delivered again \u2014 email was silently disabled across all alert types.<\/li>\n<li><strong>FIX<\/strong> Appointment form: selecting Installation no longer adds GPS and SIM device costs automatically (only labor is pre-filled now).<\/li>\n<li><strong>FIX<\/strong> Appointment form: scheduled date and time now save correctly and appear in the appointment list after booking.<\/li>\n<li><strong>ADD<\/strong> Alternatives pages now link to their head-to-head comparison page (and comparison pages link back to alternatives), so visitors exploring one angle can easily reach the other.<\/li>\n<li><strong>FIX<\/strong> AI high-intent outreach no longer repeatedly emails the same lead when the lead's account already has an active tracker running.<\/li>\n<li><strong>FIX<\/strong> Cold outbound emails (breakup, logistics sequences) no longer crash for leads whose currency preference was not set.<\/li>\n<li><strong>MOD<\/strong> Checkout address form now reveals city, state, ZIP, and country fields only after you start typing your street address, reducing visual clutter on first load<\/li>\n<li><strong>MOD<\/strong> Country dropdown on the checkout address form now matches the style of the other input fields (consistent padding, border, and a custom arrow instead of the browser default)<\/li>\n<li><strong>MOD<\/strong> Checkout form: added breathing room between the auto-detected shipping zone card and the pay button<\/li>\n<li><strong>FIX<\/strong> Store selection page now shows all available stores when no store is found near your location, instead of silently redirecting to a single store<\/li>\n<li><strong>FIX<\/strong> Installation add-on can now be removed from the cart like any other item, with the same quantity stepper and remove button<\/li>\n<li><strong>MOD<\/strong> Order summary on the checkout page is now expanded by default on mobile so your items are immediately visible<\/li>\n<li><strong>FIX<\/strong> &quot;Order now&quot; button on the product and offer pages now disables immediately after the first click, preventing duplicate orders from impatient double-taps<\/li>\n<li><strong>MOD<\/strong> Chatbot conversation view now shows a compact visitor summary (location, UTM source, engagement score, device, recent pages, lead qualification) so agents have full context without switching tabs<\/li>\n<li><strong>FIX<\/strong> Fuel level for Teltonika LLS probes now shows the last valid reading when the probe temporarily reports a disconnect sentinel, instead of showing nothing<\/li>\n<li><strong>FIX<\/strong> Trip history now shows the correct fuel level for LLS-equipped trucks instead of always showing empty (was reading a Coban-only field)<\/li>\n<li><strong>FIX<\/strong> Fuel level chart and report now include LLS probe readings alongside other sensor types, so fleet owners with RS-232\/RS-485 probes see data in their fuel reports<\/li>\n<li><strong>MOD<\/strong> Order admin: Status and Store filters are now multi-select, and a new &quot;Paid, not delivered&quot; quick filter makes fulfillment tracking easier<\/li>\n<li><strong>ADD<\/strong> <code>App\\Actions\\Founder\\SendFounderEmailAction<\/code> is now the single entrypoint for personal founder email sends (1:1 replies, manual outreach). Handles SMTP + IMAP-append to the Sent folder + CRM activity logging in one call, with threading header passthrough (<code>In-Reply-To<\/code> \/ <code>References<\/code>).<\/li>\n<li><strong>MOD<\/strong> Mail config consolidated: the <code>founder_smtp<\/code> mailer has been renamed to <code>founder<\/code> and its from-defaults are nested under <code>mail.mailers.founder.from<\/code> (previously in a separate top-level <code>mail.founder<\/code> block). Call sites updated across the app, CRM rate-limit caps, bounce\/complaint event payloads, and the <code>outbound_metrics_daily.mailer<\/code> column.<\/li>\n<\/ul>\n<h3>Week 7 \u00b7 Apr 12-18<\/h3>\n<ul>\n<li><strong>ADD<\/strong> The AI assistant now keeps your full conversation history for 28 days (up from 24 hours) and shows conversations from all channels \u2014 web, app, WhatsApp, SMS, email \u2014 in one place. No context lost between sessions.<\/li>\n<li><strong>ADD<\/strong> The AI assistant can now run a full health check on your GPS devices on request: subscription status, connectivity (online\/offline with time since last update), power issues, recent vehicle activity, and platform health \u2014 all in one reply.<\/li>\n<li><strong>ADD<\/strong> You can now ask the AI assistant to restart your GPS device or update its settings directly from the chat. The assistant asks for confirmation before acting. Useful when a device stops sending positions without a clear reason.<\/li>\n<li><strong>ADD<\/strong> Six real 5-star and 4-star Google reviews now display on the Coban 303F product page, imported from our Google Business Profile<\/li>\n<li><strong>MOD<\/strong> Lead qualification form now shows a distraction-free header (no nav links or cart) with a sticky &quot;Next&quot; button at the bottom of the screen, so the primary action is always visible without scrolling<\/li>\n<li><strong>MOD<\/strong> Topbar CTA is now context-aware across all multi-step flows: qualification, device setup, and driver onboarding show a clean header, while the thank-you page shows &quot;Add Vehicle&quot; and product pages show &quot;Order now&quot;<\/li>\n<li><strong>FIX<\/strong> Changing a lead's locale or currency in the admin panel now saves correctly. Previously the raw metadata editor was silently overwriting individual field changes<\/li>\n<li><strong>FIX<\/strong> Orders with zero-value line items no longer appear as &quot;paid&quot; when no payment was actually made<\/li>\n<li><strong>ADD<\/strong> Customers automatically receive a Google review request at their peak satisfaction moment (first GPS ping or 48h after delivery). 7 free days are added to their subscription before asking. A lighter email reminder follows 3 days later if no review was left<\/li>\n<li><strong>FIX<\/strong> Store pages, store selection, quote builder, and device activation flow were returning errors because the product filter was querying a column that does not exist. All six affected pages now load correctly<\/li>\n<li><strong>ADD<\/strong> Customers can now leave a review directly on their order page instead of being redirected into the post-purchase upsell flow. A standalone review page is also available at <code>\/order\/{id}\/review<\/code> for use in emails and links<\/li>\n<li><strong>FIX<\/strong> Orders placed by an admin on behalf of a customer no longer record the admin's location, browser, or ad cookies. Tracking data stays blank until the customer opens the payment page themselves, at which point their real browser data is captured<\/li>\n<li><strong>FIX<\/strong> Towing alerts now fire correctly when a device has many subscribed users. Previously only the first batch of subscribers received the alert, and subsequent batches could create duplicate towing events<\/li>\n<li><strong>FIX<\/strong> Third-party device payments complete successfully again. A typo in the payment job was causing it to fail silently when looking up the device cash-flow settings<\/li>\n<li><strong>FIX<\/strong> Hard-braking alerts no longer fire on stale position data. When two consecutive GPS readings are far apart in time, the acceleration calculation is now correctly discarded for braking events<\/li>\n<li><strong>FIX<\/strong> Geofence calendar schedules load correctly. The geofence model was pointing to itself instead of the calendar, so time-based geofence rules were silently ignored<\/li>\n<li><strong>FIX<\/strong> Marketing email sequences now bypass the non-transactional rate limiter. Abandoned cart, checkout page abandoned, win-back, device activation, and customer success emails are time-sensitive and no longer get throttled alongside bulk marketing sends<\/li>\n<li><strong>FIX<\/strong> Geo-zone radius is now clamped between 0 and 50,000 meters. Previously any value was accepted, which could cause map rendering issues with absurdly large zones<\/li>\n<li><strong>FIX<\/strong> Export downloads now require authentication and sanitize the filename to prevent path traversal<\/li>\n<li><strong>FIX<\/strong> OAuth account-linking routes (Facebook, Instagram, TikTok, Twitter, LinkedIn) now require authentication. Previously these endpoints were accessible without logging in<\/li>\n<\/ul>\n<h3>Week 6 \u00b7 Apr 5-11<\/h3>\n<ul>\n<li><strong>ADD<\/strong> Senegalese buyers (Wave, Orange Money) can now check out with just a shipping zone and payment method \u2014 no form to fill in. Your name, phone, and account are created automatically after payment completes using the phone number embedded in the payment confirmation.<\/li>\n<li><strong>MOD<\/strong> On mobile, a fixed pay bar slides up from the bottom of the screen when the checkout button scrolls out of view. Shows the total price and a pay button so you can always pay without scrolling back up on long checkout forms.<\/li>\n<li><strong>FIX<\/strong> Chatbot now emails the quote automatically when a visitor asks for one. Previously the bot generated a shareable URL and asked the visitor to copy it themselves, which was burning hot B2B leads including a 129M FCFA\/year Guinean fleet request<\/li>\n<li><strong>FIX<\/strong> Chatbot keeps the visitor's language steady across an entire conversation. Short single-word replies like &quot;oui&quot;, &quot;Fcfa&quot;, or a city name no longer flip a French conversation into English or Spanish mid-chat, and new anonymous web sessions now default to French instead of picking up whatever the site's locale negotiator happened to return (which was sometimes Spanish)<\/li>\n<li><strong>MOD<\/strong> Chatbot on the website now hands off to a human immediately on payment issues, reseller\/partner inquiries, specialized-accessory compatibility questions (fuel sensors, OBD2, analog inputs), large-fleet quotes (more than 20 vehicles or 5 000 000 FCFA\/year), and installation requests from outside Dakar metro or Toronto metro. Each of those used to get stuck in qualification loops, permission-asking, or fabricated coverage promises<\/li>\n<li><strong>MOD<\/strong> Chatbot closes the loop on buying-interest conversations. After a visitor has shown intent, the bot now offers a concrete next step (quote by email, lead capture, or a direct order call-to-action) instead of ending on a passive &quot;n'h\u00e9sitez pas si vous en avez d'autres&quot;. Bare-greeting openers also get three soft options up front instead of a generic &quot;how can I help?&quot;<\/li>\n<li><strong>MOD<\/strong> Chatbot answers pricing in XOF by default, leads with Premium across every industry (including personal and family), and keeps technical answers technical. The Free plan only comes up when the visitor explicitly raises cost as a blocker, and on the &quot;Prot\u00e9gez votre voiture&quot; offer page the bot reframes the price objection around the already-included year of Premium<\/li>\n<li><strong>MOD<\/strong> Chatbot no longer invents Traxelio metrics. If asked for a customer count, recovery rate, alert latency, or any stat it can't verify from its knowledge base, it says so and connects the visitor with the team instead of fabricating a number<\/li>\n<li><strong>FIX<\/strong> Agent takeover on web conversations now sticks. When an agent replies from the admin panel, the conversation is atomically marked as handed off, so the bot stops racing the human on the next visitor message<\/li>\n<li><strong>FIX<\/strong> Admin: subscription-plan line items on an order can be edited again. Clicking the edit icon on those rows was silently failing because one legacy subscription plan had no currency set, and the product dropdown crashed trying to render its price<\/li>\n<li><strong>FIX<\/strong> The AI chatbot no longer dead-ends with &quot;Je suis temporairement indisponible&quot; when the model fails or returns an empty reply. Any failure now auto-escalates the conversation to a human agent and shows the visitor a &quot;un conseiller va vous rejoindre&quot; bridge message, so visitors are not abandoned mid-conversation. Previously hit roughly 24% of substantive chat sessions, including a single-word-collapse pattern (e.g. replying &quot;Fcfa&quot;, &quot;oui&quot;, or &quot;Dakar&quot;) that was killing warm leads<\/li>\n<li><strong>ADD<\/strong> Duplicate action on <code>SocialPostResource<\/code> as both row and bulk action. Replicates the post as a DRAFT (clears <code>status<\/code>, <code>scheduled_at<\/code>, <code>published_at<\/code>, <code>meta<\/code>), prepends <code>(Copy)<\/code> to the title, re-syncs target channels with a clean pivot (no stale external IDs\/statuses), and redirects the single-row flow to the new draft's edit page. Lets the team spin up near-identical variants (A\/B captions, platform tweaks) without re-filling the whole form<\/li>\n<li><strong>FIX<\/strong> Post-purchase order page shows the &quot;Schedule your installation&quot; call-to-action again when the order includes the installation add-on. Customers who bought the GPS + installation bundle were silently not being prompted to book their technician appointment<\/li>\n<li><strong>FIX<\/strong> Review text now renders on marketplace driver profile pages and gig review cards. A schema rename on April 1 left two templates reading a field that no longer exists, so every driver's reviews appeared blank even when the star rating was correct<\/li>\n<li><strong>FIX<\/strong> At-risk retention emails (device-silent-7-days, user-inactive-14-days, milestone unlocks) resume firing. Four internal jobs were querying with a stale morph key and silently running on zero customers, so nobody was getting the &quot;your tracker has been silent for a week, here's how to fix it&quot; email<\/li>\n<li><strong>MOD<\/strong> Offer-originated purchases are now correctly tagged as <code>target = offer<\/code> in internal reporting (previously grouped with landing-page purchases, which made offer funnel performance impossible to measure). Forward-only; historical orders cannot be re-classified<\/li>\n<li><strong>ADD<\/strong> Two parallel-subagent code review commands: <code>\/rot-hunt<\/code> (weekly monorepo sweep \u2014 25 parallel read-only reviewers across 16 <code>lib\/<\/code> packages + 8 <code>app\/<\/code> shards + 1 toolchain runner, each applying the 5-concern rubric of security\/performance\/architecture\/domain-logic\/simplification; full per-package findings print to terminal, Critical\/High get appended to <code>tasks\/todo.md<\/code>) and <code>\/ship-review<\/code> (per-session manual trigger \u2014 4 parallel reviewers covering 100% of <code>docs\/process\/code-review.md<\/code>: Ship-Safety owns Security+Edge Cases+Robustness+deploy+CAPI+mobile parity, Code-Quality owns Performance+Simplicity+Usage Tracking+test coverage, Architecture-Coherence owns convention drift+duplication+boundary correctness, Toolchain owns the Final Gate via <code>composer lint<\/code>\/<code>analyze<\/code>\/<code>test<\/code>; scopes the full feature branch via <code>git diff main...HEAD<\/code> + <code>git log main..HEAD<\/code> so reviewers see the commit story not just the final state; rubric coverage checklist in terminal output guarantees no section orphaned). Research-backed split based on HAMY's 9-agent <code>\/code-review<\/code> pattern, RKoots enterprise scaling post, and Addy Osmani's &quot;Code Agent Orchestra&quot; consensus that specialist-per-concern outperforms generalist. Sharding by <code>lib\/<\/code> package (not by concern across the whole tree) was a deliberate divergence from the research to fit the monorepo's ownership boundaries<\/li>\n<li><strong>FIX<\/strong> <code>visitor_activities<\/code> MongoDB entity_type hygiene cleanup for the reporting funnel (todo #44). Surfaced during the Meta CAPI funnel audit. The <code>HasViews<\/code> trait hardcoded <code>PRODUCT_VIEWED<\/code> so every blog post view on <code>app\/Models\/Post.php<\/code> wrote <code>activity_type = product_viewed + entity_type = &quot;post&quot;<\/code> \u2014 4,760 rows contaminating the product funnel. <code>TrackVisitorMiddleware<\/code> also wrote <code>product_viewed<\/code> with null metadata for route hits matching <code>product.show<\/code>\/<code>product.index<\/code>\/<code>product.landing<\/code>. <code>Offer::recordView()<\/code> wrote <code>entity_type = $this-&gt;getMorphClass()<\/code> which returned the namespaced FQCN <code>Neptune\\Domains\\Marketplace\\Models\\Offer<\/code> (13,072 rows) because there was no morphMap entry. Legacy <code>store_product<\/code> entity_type values (63,570 rows) from the pre-morphMap era when the table was still named <code>store_products<\/code> were also frozen in place. Fix: (1) added <code>VisitorActivityTypeEnum::POST_VIEWED<\/code>; (2) <code>HasViews<\/code> now has a <code>viewActivityType()<\/code> method (defaults to <code>PRODUCT_VIEWED<\/code>) that consuming models can override; (3) <code>Post<\/code> overrides it to return <code>POST_VIEWED<\/code>; (4) <code>TrackVisitorMiddleware::resolveActivityType()<\/code> no longer writes <code>product_viewed<\/code> for product route names \u2014 they fall through to <code>page_view<\/code> and the real product view is written by the model-level <code>$product-&gt;recordView()<\/code> in the controller; (5) <code>Offer::recordView()<\/code> hardcodes <code>entity_type = &quot;offer&quot;<\/code> instead of deriving from the morph class. Note: Meta CAPI is completely decoupled from <code>visitor_activities<\/code> \u2014 the <code>ProductViewedEvent<\/code>\/<code>OfferViewedEvent<\/code> listeners that fire <code>ViewContent<\/code> to Meta were never polluted by any of this, only the internal funnel reporting was<\/li>\n<li><strong>MOD<\/strong> Run <code>trackers:audit --only=offline<\/code> every 30 min as a backstop for the realtime offline path (<code>SocketListenCommand<\/code> \u2192 <code>DeviceStatusListener<\/code> \u2192 <code>ProcessDeviceStatusJob<\/code>). Before this change, the full <code>trackers:audit<\/code> command ran once a day at 06:30 \u2014 which meant a tracker that died silently (low battery, disconnected, no Traccar event at all) could go up to ~24h before the safety-net audit fired <code>DeviceOfflineEvent<\/code> and notified the opted-in customer. With the 30 min cadence, worst-case latency drops to ~30 min. The full audit (timestamp \/ airtime \/ billing \/ stalled \/ silent checks) still runs once daily at 06:30 since those are expensive and only need daily accuracy. Both schedules use <code>withoutOverlapping()<\/code> to prevent double runs. The 18 min heartbeat threshold (<code>config('traxelio.tracker.heartbeat.threshold')<\/code>) is unchanged \u2014 a tracker offline for less than 18 min still gets filtered out as a connectivity blip. Follow-up to the <code>DeviceOfflineNotification<\/code> customer rollout shipped in the previous commit<\/li>\n<li><strong>MOD<\/strong> Merge the <code>TrackerInactive<\/code> dead branch into the live <code>DeviceOffline<\/code> notification path so the 11 customers (10 distinct users, 11 devices) who already opted in via the mobile <code>tracker_inactive<\/code> toggle finally receive offline alerts \u2014 before this change, <code>TrackerInactiveEvent<\/code> existed in code but nothing dispatched it, so the settings rows in <code>device_has_settings<\/code> did nothing. (1) Rewrote <code>NotifyForOfflineTracker<\/code> to use the <code>NotifiesSettingOwners<\/code> trait and call <code>notifySettingOwners($device, SettingsTypeEnum::TRACKER_INACTIVE, ...)<\/code> instead of the hardcoded <code>babacar@traxelio.com<\/code> notify + commented-out chunk loop. Founder Slack log kept as a separate 24h per-device rate-limited call so global visibility survives. (2) Enabled mail in <code>DeviceOfflineNotification::via()<\/code> (was temp-disabled with a TODO) and replaced the previous &quot;renew subscription&quot; mail copy with the troubleshooting-focused copy ported from <code>TrackerInactiveNotification<\/code> (greeting by name, last-seen with diffForHumans, last known position, &quot;What you can do now&quot; bullets for power source + drive-to-recharge, action button to the device page, reply-for-support footer). Adapted to use <code>$device-&gt;last_seen<\/code> since <code>DeviceOfflineEvent<\/code> has no Event model. (3) Added <code>-&gt;bcc(&quot;babacar@traxelio.com&quot;)<\/code> on both <code>DeviceOfflineNotification::toMail()<\/code> and <code>TrackerActiveNotification::toMail()<\/code> so the founder still gets a copy of every tracker active\/inactive email without the hardcoded User lookup. (4) Deleted <code>TrackerInactiveEvent<\/code>, <code>NotifyForTrackerInactive<\/code>, <code>TrackerInactiveNotification<\/code> and their mapping in <code>EventServiceProvider<\/code> (nothing dispatched the event anyway). (5) Fixed a dangling <code>TrackerInactiveNotification::class<\/code> reference inside <code>NotifyForTrackerActive<\/code>'s rate-limit key (replaced with a string literal to preserve the existing cache key schema). All settings controller + policy + API routes + mobile app toggle already existed and continue to work unchanged \u2014 zero mobile work. Per-user 24h rate limit is enforced by <code>SettingsTypeEnum::TRACKER_INACTIVE-&gt;rateLimitSeconds()<\/code><\/li>\n<li><strong>MOD<\/strong> Rewrite SEO title + meta description for 7 pSEO location pages surfaced by <code>gsc detect_quick_wins<\/code>: <code>\/gps-tracking\/cote-divoire\/abidjan<\/code> (<code>gps abidjan<\/code> query, 166 impr\/pos 7.5), <code>\/gps-tracking\/united-states\/chicago<\/code> (<code>gps tracking chicago<\/code>, 124 impr\/pos 7.1), <code>\/gps-tracking\/maroc\/marrakech<\/code> (<code>gps marrakech<\/code>, 105 impr\/pos 7.2), <code>\/gps-tracking\/maroc\/casablanca<\/code> (<code>gps casablanca<\/code>, 61 impr\/pos 8.6), <code>\/gps-tracking\/ethiopia\/addis-ababa<\/code> (<code>gps addis ababa<\/code>, 57 impr\/pos 8.5), <code>\/gps-tracking\/gambie<\/code> (<code>gps gambia<\/code>\/<code>gps banjul<\/code> intent, concedes the off-topic ferry queries), <code>\/gps-tracking\/mali<\/code> (<code>gps bamako<\/code>, 109 impr\/pos 4.8). All titles tightened to 52-59 chars (SERP limit ~60) with the head query front-loaded (&quot;GPS {City}&quot; pattern). All descriptions 141-153 chars, front-loading the head noun + local context + concrete benefits (live location, trip history, geofence alerts, engine cut-off). No &quot;real-time&quot; phrases, no em-dashes. Edits live in <code>lib\/p-seo\/database\/data\/cities\/{CI,US,MA,ET}.json<\/code> and <code>lib\/p-seo\/database\/data\/countries\/africa.json<\/code>. <strong>Requires <code>LocationSeeder<\/code> re-run on prod<\/strong> to push into DB (blade templates read meta_title\/meta_description columns from Eloquent, not the JSON directly). Combined GSC-projected lift: ~40 clicks\/month once Google re-crawls<\/li>\n<li><strong>MOD<\/strong> Harden <code>StoreSeeder::seedOffers()<\/code> against the class of silent-drift bug that caused 44\/48 prod <code>offer_items<\/code> to ship with <code>product_id = null<\/code>. (1) Replace <code>if (! $gps) return;<\/code> with a <code>RuntimeException<\/code> naming the missing slug and the required prerequisite, so future slug drifts fail at seed time instead of silently producing broken data. (2) Apply the same throw to the <code>$fuelSensor<\/code> lookup (was a nullable <code>?-&gt;id<\/code> chain that would silently null fuel lines). (3) Replace <code>Offer::firstOrCreate<\/code> with <code>updateOrCreate<\/code> so edits to name\/description\/price\/published flag actually propagate on re-seed (prod's <code>gps-basic-mensuel<\/code> and <code>gps-basic-annuel<\/code> had been manually published via Filament after the initial seed and the seeder never caught up). (4) Replace the <code>if ($offer-&gt;items()-&gt;exists()) continue;<\/code> short-circuit with <code>$offer-&gt;items()-&gt;delete()<\/code> + recreate so label\/price\/product_id edits in the offers array propagate instead of freezing the first seed (this is exactly how the &quot;GPS Tracker Coban 303&quot; \u2192 &quot;Coban 303F&quot; rename would have failed to propagate). (5) Swap raw <code>&quot;basic&quot;<\/code> \/ <code>&quot;one_month&quot;<\/code> string literals for <code>PlanEnum::BASIC-&gt;value<\/code> \/ <code>PlanDurationEnum::ONE_MONTH-&gt;value<\/code> so enum drift fails at seed time. Also add <code>&quot;published&quot; =&gt; true<\/code> to the Basic Mensuel + Basic Annuel offers to match prod state<\/li>\n<li><strong>FIX<\/strong> Delete orphan draft products <code>fuel-sensor-300cm<\/code>, <code>fuel-sensor-70cm<\/code>, and abandoned test order <code>01knmxbepf33y8my82jp44be8s<\/code> (1.4M XOF unpaid Platinum annuel truck prospect) from the Dakar store. All part of the offer_items backfill cleanup<\/li>\n<li><strong>FIX<\/strong> Email queue routing consolidation: jobs dispatched via <code>DispatchOutboundSequenceAction<\/code> were landing in Redis queue <code>traxelio-production-emails<\/code> (from <code>config(&quot;queue.emails&quot;)<\/code>) while the <code>queue-redis-emails<\/code> supervisor worker consumed only <code>emails<\/code>, orphaning 4,057 jobs (44 of them fresh intros from tonight's outbound batch across canada-gta\/us-austin\/us-miami\/us-houston\/us-dallas). Root cause was a config\/worker drift, not a code bug. Fix (this commit): make <code>config\/queue.php<\/code> read <code>env('SQS_QUEUE_EMAILS')<\/code> with <strong>no fallback<\/strong> so the env is the single source of truth and missing config fails fast instead of silently routing to the wrong queue. Propagate the canonical <code>traxelio-production-emails<\/code> queue name through <code>docker-compose.yml<\/code>, <code>devops\/docker\/supervisor\/conf.d\/queue-redis-emails.conf<\/code>, <code>devops\/scripts\/healthcheck.sh<\/code>, <code>.env.example<\/code>, <code>phpunit.xml<\/code>, and every doc that referenced the old <code>emails<\/code> name (<code>docs\/architecture\/index.md<\/code>, <code>docs\/architecture\/monitoring.md<\/code>, <code>docs\/infra\/stack.md<\/code>, <code>docs\/orchestrations\/atoms.md<\/code>, <code>.claude\/skills\/newsletter.md<\/code>). Fix remaining hardcoded <code>-&gt;onQueue(&quot;emails&quot;)<\/code> in <code>app\/Observers\/OrderObserver.php:34<\/code> (delivered-order review request path) to use <code>config(&quot;queue.emails&quot;)<\/code> like every other dispatch site. Apply the same no-fallback pattern to <code>queue.crm<\/code>, <code>queue.batches<\/code>, <code>queue.renders<\/code> for consistency<\/li>\n<li><strong>MOD<\/strong> Remove <code>RateLimitPerFunnel<\/code> middleware from <code>SendSequenceEmailJob::jobMiddleware()<\/code>. The per-funnel cap of 100\/day did nothing to smooth <em>per-minute<\/em> burst rates: when 4 workers drained the orphaned queue tonight, ~318 intros fired in 5 minutes across 4 funnels before the daily counter hit its cap. A shared-domain sender with transactional mail flowing at 500-2000\/day cannot absorb that kind of burst without reputation damage. Replacement work tracked in <code>tasks\/todo.md<\/code> HIGH section: a single global non-transactional rate limiter that combines per-day + per-minute caps, uses atomic Lua INCR (the old middleware had a <code>GET \u2192 if check \u2192 INCR \u2192 EXPIRE<\/code> race that let <code>outbound_logistics_delivery<\/code> hit 101\/100), and a <code>ShouldBypassRateLimit<\/code> interface for transactional carve-outs. <code>RateLimitPerFunnel.php<\/code> class file kept in place as reference for the rewrite; just no longer wired into the middleware chain<\/li>\n<li><strong>ADD<\/strong> 9 new chain exclusions to <code>docs\/outbound\/chains.md<\/code> discovered during tonight's 5-region outbound dispatch: <code>AutoNation<\/code> (NYSE: AN, 325+ dealership chain), <code>Office Depot<\/code> (800+ stores), <code>Schneider Truck<\/code>\/<code>Schneider National<\/code> (Fortune 500 trucking, 12k+ trucks), <code>SelecTrucks<\/code> (Daimler Trucks NA dealer network), <code>UPS Alliance<\/code> (UPS franchise partner), <code>Reliable Couriers<\/code> (60+ city national chain), <code>Avondale Premier Collection<\/code> (Avondale Group, 8 corporate luxury dealerships), <code>Premier Truck Group<\/code> (Freightliner corporate, 30+ locations)<\/li>\n<li><strong>FIX<\/strong> Marketplace StoreSeeder <code>seedOffers()<\/code> was silently bailing on Dakar store: the GPS product lookup used stale slug <code>coban-gps303<\/code> but the actual product slug is <code>coban-303f<\/code>, so <code>$gps<\/code> came back null and the <code>if (! $gps) return;<\/code> guard aborted offer seeding. Result: 44 of 48 prod <code>offer_items<\/code> had <code>product_id = null<\/code> (including all Coban 303F hardware lines across every offer + both <code>gps-*<\/code> bundles and <code>pack-*<\/code> packs). Also replaced the wildcard <code>like &quot;fuel-sensor%&quot;<\/code> fuel sensor lookup (which picked up a draft SKU first by ULID order) with explicit <code>aooog-ag-af-fuel-sensor-700mm<\/code>. Deleted the unused <code>fuel-sensor-300cm<\/code> product entry (no paid orders, no images, no bundles); folded its deep-tank theft pain-point angle into the published aooog description. Relabeled all <code>&quot;GPS Tracker Coban 303&quot;<\/code> \u2192 <code>&quot;Coban 303F&quot;<\/code> and <code>&quot;GPS Tracker Coban 303 (offert)&quot;<\/code> \u2192 <code>&quot;Coban 303F (offert)&quot;<\/code> in seeder offer items to match the canonical product name. Prod data migration ran in a transaction alongside: backfilled 16 GPS hardware rows \u2192 <code>coban-303f<\/code>, repointed 4 fuel rows \u2192 aooog, deleted the fuel-sensor-300cm product + 2 cart_items + 1 product_addons pivot referencing it<\/li>\n<li><strong>MOD<\/strong> pSEO tracker brand page template: title <code>&quot;{Brand} GPS Trackers - Traxelio&quot;<\/code> (30 chars, generic) \u2192 <code>&quot;{Brand} GPS Tracker: Setup &amp; Live Tracking | Traxelio&quot;<\/code> (51-61 chars across all brand name lengths, front-loads query intent). Meta description rewritten to remove a banned &quot;real-time tracking&quot; phrase and front-load the value props. Cascades to all ~520 brand pages. GSC data for <code>\/trackers\/motive<\/code>: 278 combined impressions at position 7-9 with 0 clicks; expected +14 clicks\/month once Google re-crawls<\/li>\n<li><strong>FIX<\/strong> <code>\/trackers\/goome\/goome-goome<\/code> was cannibalizing the <code>\/trackers\/goome<\/code> brand page for the <code>goome<\/code> query (333 impressions, pos 4.4, 0 clicks). Both pages rendered the same content because the device row was a generic brand-level entry scraped without a specific model. Removed the row from <code>database\/data\/tracker-devices.json<\/code>, added a 301 redirect in <code>PSeoServiceProvider<\/code> so all ranking signal consolidates on the brand page. Requires <code>trackers:import<\/code> re-run + orphan row delete on prod<\/li>\n<li><strong>FIX<\/strong> pSEO C\u00f4te d'Ivoire city pages had accent-stripped French titles and descriptions (<code>&quot;Geolocalisation GPS a Abidjan&quot;<\/code> instead of <code>&quot;G\u00e9olocalisation GPS \u00e0 Abidjan&quot;<\/code>), tanking CTR on queries like <code>gps abidjan<\/code> (166 impressions at position 7.5, 0 clicks). Rewrote all 5 CI city meta_title + meta_description in <code>lib\/p-seo\/database\/data\/cities\/CI.json<\/code> with proper accents, front-loaded <code>GPS {City}:<\/code> format, and shortened to under 60 chars. Also removed a banned &quot;en temps reel&quot; phrase from the Korhogo description. Requires <code>LocationSeeder<\/code> re-run on prod to push into DB<\/li>\n<li><strong>FIX<\/strong> Revert <code>Product<\/code> + <code>aggregateRating<\/code> JSON-LD from shared templates (<code>locations\/city.blade.php<\/code>, <code>locations\/country.blade.php<\/code>, <code>comparisons\/show.blade.php<\/code>) - Google flags duplicate Product structured data when the same aggregateRating and review content appears across 18+ pages. Kept the BreadcrumbList + Service JSON-LD refactor (legitimate escaping bug fix). Product schema now only lives on <code>\/pricing<\/code> where it belongs - a single canonical product page<\/li>\n<li><strong>MOD<\/strong> pSEO city + country page templates: refactored existing BreadcrumbList + Service JSON-LD blocks as PHP arrays emitted via <code>json_encode<\/code> to prevent unparseable structured data when city\/country descriptions contain quotes or entities (same pattern as the earlier homepage + comparison fixes)<\/li>\n<li><strong>MOD<\/strong> <code>\/guides\/gps-tracker-troubleshooting<\/code> SERP optimization (second biggest CTR leak): title 69\u219259 chars, meta description 164\u2192142 chars, front-loading the problem vocabulary that matches long-tail troubleshooting queries. Added <code>Article<\/code> JSON-LD to the shared <code>guides\/show.blade.php<\/code> template (cascades to all 5 guides) with headline, description, image, author, publisher, and fixed publish\/modified dates. GSC data showed 1,241 impressions\/month at position 7 with 0.40% CTR \u2192 potential +40 clicks\/month once Google re-crawls<\/li>\n<li><strong>MOD<\/strong> <code>\/compare\/traxelio-vs-motive<\/code> SERP optimization: title 77\u219258 chars (drops the auto-generated &quot;$25.00&quot; format for &quot;$25-$40\/mo&quot;), meta description 197\u2192128 chars front-loading the pricing answer. Added <code>Product<\/code> + <code>aggregateRating<\/code> + <code>review<\/code> JSON-LD (4.7\/5 from 29 reviews, same pinned constants as \/pricing) so the SERP shows star ratings. GSC data showed 2,689 impressions\/month at position 4.3 with 0.60% CTR - potential recovery ~150 clicks\/month once Google re-crawls<\/li>\n<li><strong>FIX<\/strong> <code>comparisons\/show.blade.php<\/code> FAQPage JSON-LD: same <code>Js::from()<\/code>-style unparseable bug as <code>element\/page\/faq.blade.php<\/code> - question\/answer strings embedded via blade interpolation without escaping, broken by any quote or entity. Replaced with <code>json_encode()<\/code> pattern. Affects all 18 <code>\/compare\/traxelio-vs-*<\/code> pages<\/li>\n<li><strong>ADD<\/strong> <code>ComparisonEnum::pShort()<\/code> helper that strips trailing <code>.00<\/code> from currency formatting for tight title\/meta contexts. Prevents the auto-generated &quot;$25.00&quot; look that was hurting SERP CTR<\/li>\n<li><strong>FIX<\/strong> <code>public\/images\/og-default.png<\/code> regenerated from <code>og-default.svg<\/code> (1200x630, 136 KB). The referenced path in <code>meta-head.blade.php<\/code> had been 404ing on prod, breaking the default OpenGraph card on every page without a <code>@section('image')<\/code> override (Facebook, WhatsApp, LinkedIn, iMessage previews). Generated via <code>rsvg-convert<\/code> from the existing branded SVG so the card shows headline, subtitle, phone mockup, and 6 benefit icons<\/li>\n<li><strong>FIX<\/strong> Homepage + \/faq FAQPage structured data: <code>Js::from()<\/code> was outputting single-quoted JS strings instead of valid JSON, causing GSC to flag the homepage as &quot;Unparsable structured data \u00b7 Incorrect value type&quot;. Replaced with <code>@json()<\/code>\/<code>json_encode()<\/code> in both <code>element\/page\/faq.blade.php<\/code> (included on homepage and many pSEO pages) and <code>page\/faq.blade.php<\/code><\/li>\n<li><strong>MOD<\/strong> Pricing Product schema: add <code>image<\/code>, <code>shippingDetails<\/code>, and <code>hasMerchantReturnPolicy<\/code> to clear Google Search Console Merchant Listings ERROR (missing image) and 6 warnings (shipping\/return policy on each of 3 offers). Subscription-appropriate values: instant digital delivery, 14-day free refund<\/li>\n<li><strong>FIX<\/strong> Favicon: root <code>\/favicon.ico<\/code> was 0 bytes since April 2023, causing Google SERP to show a generic globe for all Traxelio pages. Replaced with valid multi-size ICO, added explicit <code>&lt;link&gt;<\/code> declaration, 192x192 PNG ref for Google's multiple-of-48 requirement, <code>theme-color<\/code> meta, and brand pink in webmanifest<\/li>\n<li><strong>MOD<\/strong> Pricing page Product JSON-LD: add <code>aggregateRating<\/code> (4.7\/5 from 29 reviews) and 3 <code>review<\/code> entries to clear Google Search Console &quot;missing optional field&quot; warnings, plus a visible &quot;Trusted by 60+ fleet owners&quot; social proof strip (numbers pinned in-blade to avoid DB queries on every render, sourced from prod all-time distinct GPS buyers). Also swap &quot;Real-time GPS tracking&quot; \u2192 &quot;Live GPS tracking&quot; in the included-features list<\/li>\n<li><strong>ADD<\/strong> NewReviewNotification + ReviewObserver: notify admin via mail and WhatsApp when a new Review is created (was silent). Guest model now implements <code>WhatsAppNotifiable<\/code> so admin notifications actually reach WhatsApp instead of log-skipping at the channel<\/li>\n<li><strong>ADD<\/strong> Creativa scene component library (<code>src\/components\/scene\/<\/code>): reusable TerminalPanel, CheckRow, QueueBar, StatCard, LeadCard, DeviceMapDot, CenteredScene + SCENE_PALETTE\/SCENE_TEXT constants. Bakes in font sizes, colors, padding, centering so future founder-style videos stay consistent without inline magic numbers<\/li>\n<li><strong>ADD<\/strong> MorningOpsVideo: 24s founder build-in-public video (7 scenes: 06:00 hook, Sentry, queues, orders, fleet map with dark Dakar tile, leads, close), first consumer of the scene library<\/li>\n<li><strong>MOD<\/strong> Skills (video-render, schedule-post, caption-for-socials): point video-render at the scene component library instead of inline size rules, change schedule-post default from &quot;next available window&quot; to &quot;next day after last scheduled, 12:00 UTC&quot; (one-post-a-day cadence), caption-for-socials defaults to ONE unified caption across all platforms<\/li>\n<li><strong>MOD<\/strong> Content calendar + backlog: remove &quot;63% en ligne&quot; (dashboard stat, wrong format for short-form), drop 4 queued items (install demo, email support, chatbot diagnose, cold outbound), move &quot;Suivez flotte motos&quot; and &quot;Geo-zones&quot; to evergreen recurrent templates, cascade calendar shifts through early May<\/li>\n<li><strong>ADD<\/strong> GPS ingestion Phase 0: real AWS SQS client, per-device sequential processing, duplicate detection (INSERT IGNORE + unique index), supervisor config, deploy script, shadow:compare Laravel cron<\/li>\n<li><strong>ADD<\/strong> Daily ops: GPS shadow compare section for monitoring Go vs Traccar match rate<\/li>\n<li><strong>ADD<\/strong> Creativa: TaxiOwner and FuelProbe carousel+video compositions (ICP-targeted, enum-driven copy, PAS framework) with Coban 303 + Aooog fuel sensor product shots<\/li>\n<li><strong>MOD<\/strong> Skills: carousel-ad, caption-for-socials, schedule-post, video-render updated with enum-driven copy workflow, fact-check hard gate, production workflow, CRF 16 default<\/li>\n<li><strong>FIX<\/strong> TikTokStudioClient: add publishWithRetry with 15s initial wait + 5 retries on code 4 (video not yet processed after upload)<\/li>\n<li><strong>FIX<\/strong> S3 stores disk: remove ACL visibility flag that broke product image conversions on BucketOwnerEnforced S3, harden ProcessStoreProductMediaJob to check put() return value<\/li>\n<li><strong>ADD<\/strong> Playwright POC for TikTok Studio upload automation (scripts\/tiktok-poc\/)<\/li>\n<li><strong>ADD<\/strong> H02 (Sinotrack) protocol parser for Go GPS ingestion service: heartbeat echo, V1 position parsing with DMM coordinate conversion, validity flag, status word attributes, and full TDD test suite (3 tests, all passing)<\/li>\n<li><strong>ADD<\/strong> GPS103 (Coban) protocol parser for Go GPS ingestion service: login, heartbeat, and position parsing with NMEA coordinate conversion, alarm type mapping, and full test coverage (5 tests, all passing)<\/li>\n<li><strong>MOD<\/strong> Order items: shopping cart UX (product type &gt; store &gt; product cascade with auto-fill), Dakar store default, auto-set order data.target and item data.duration metadata for processing pipeline compatibility<\/li>\n<li><strong>MOD<\/strong> Store seeder: merge Coban GPS303 into 303F, drop Capteur 70cm (merged into Aooog), add published flags, seed Traxelio stores on deploy via DatabaseSeeder<\/li>\n<li><strong>MOD<\/strong> Product specs: render arbitrary extra specs from product data (supports fuel sensor detailed specs like accuracy, power supply, probe length)<\/li>\n<li><strong>ADD<\/strong> Feature lifecycle: 6-phase development system (brainstorm, plan, arch+security review, execute, code review pipeline, verify) with 3 new skills (plan-review, architecture-review, security-review) and upgraded self-review pipeline<\/li>\n<li><strong>MOD<\/strong> Meta CAPI: deterministic event IDs for deduplication (purchase_{id}, subscribe_{id}, view_{id}, cart_{id}, checkout_{id}) in live listeners and backfill command<\/li>\n<li><strong>ADD<\/strong> Meta CAPI backfill command (<code>ads:meta:backfill<\/code>): replay Purchase, Subscribe, ViewContent, AddToCart, InitiateCheckout events to Meta for the last 7 days<\/li>\n<li><strong>FIX<\/strong> Order attribution: create visitor from order data when fingerprint lookup fails, so purchase activities and revenue are always attributed (was silently dropping 85\/87 buyer attributions)<\/li>\n<li><strong>FIX<\/strong> UTM attribution: pass utm_source\/medium\/campaign\/content\/term\/id and referrer in product\/offer recordView(), so ad visitors landing on excluded routes get proper attribution<\/li>\n<li><strong>MOD<\/strong> Lifetime plan: add benefit description (&quot;Pay once, track forever&quot;) on duration picker to sell the value proposition<\/li>\n<li><strong>ADD<\/strong> US store (Atlanta, GA): seeder with Concox JM-VL02 ($89) + GT06N ($79), USD pricing, Stripe\/PayPal, 3 shipping zones (GA free, Southeast $7.99, Continental $12.99), installation addon $75<\/li>\n<li><strong>MOD<\/strong> Toronto store: remove third-party products (Bouncie, LandAirSea, Tracki Pro, PlanetGPS), keep only platform-connected Concox devices<\/li>\n<li><strong>ADD<\/strong> US expansion plan: Miami region seeded (236 companies), partner gap analysis, Queclink GV500MAP as premium tier candidate, Hologram as primary SIM provider<\/li>\n<li><strong>FIX<\/strong> Outbound locale: default to USD\/English instead of XOF\/French, set currency from company country on lead creation, correct USD pricing ($89)<\/li>\n<li><strong>MOD<\/strong> Outbound dispatch: skip AI intro generation when <code>custom_intro<\/code> already exists in lead metadata (supports Claude Code pre-personalization)<\/li>\n<li><strong>MOD<\/strong> Outbound dispatch skill: document AI personalization flow (Claude Code generates intros before dispatch), CTA button policy, locale\/currency strategy<\/li>\n<li><strong>MOD<\/strong> IoT SIM provider docs: consolidate duplicate files, link from infrastructure\/sims.md<\/li>\n<li><strong>MOD<\/strong> Lifetime pricing: standardize to 2.5x monthly (industry standard). Basic 180k, Premium 300k, Platinum 3.75M XOF. Add Platinum yearly to pricing table<\/li>\n<li><strong>ADD<\/strong> Blog post: &quot;Abonnement a vie GPS Traxelio&quot; (FR + EN) with full pricing table, savings comparison, FAQ<\/li>\n<li><strong>FIX<\/strong> Route ReviewRequestNotification (48h delay) to Redis\/emails queue to avoid SQS 900s delay limit<\/li>\n<li><strong>ADD<\/strong> Checkout: show yearly plan description (&quot;Pay for 10 months, 2 for free&quot;) on duration picker to highlight savings<\/li>\n<li><strong>ADD<\/strong> New sales segments: NGO\/humanitarian, agricultural cooperatives, government fleets, auto dealerships. Full ICPs, outbound email drafts, 28 companies imported to CRM, NGO_HUMANITARIAN industry enum + outbound funnel with 5 French notification classes<\/li>\n<li><strong>MOD<\/strong> <code>crm:process-bounces<\/code>: classify remote-host 4xx SMTP rejections as soft bounces so they are deleted from mailbox without marking the recipient as bounced (previously left unclassified in inbox)<\/li>\n<li><strong>ADD<\/strong> Omni Post: bulk delete and admin-only delete on SocialPost resource (table, edit page, bulk action)<\/li>\n<li><strong>MOD<\/strong> Chatbot: hide chat widget on <code>product.show<\/code> and <code>cart.show<\/code> pages to avoid interfering with purchase flow<\/li>\n<li><strong>MOD<\/strong> Docs: restructure docs\/ into semantic folders (architecture\/, company\/, infrastructure\/, process\/, playbooks\/) with zero loose root files<\/li>\n<li><strong>ADD<\/strong> Playbooks: consolidated content playbook, Google Ads playbook, Hormozi precision-over-volume strategy in meta-ads playbook<\/li>\n<li><strong>MOD<\/strong> Tasks: move loose scripts to tasks\/scripts\/, legacy specs deleted<\/li>\n<li><strong>MOD<\/strong> Skills: update all skill\/orchestration\/operation references to new playbook and doc paths<\/li>\n<\/ul>\n<h3>Week 5 \u00b7 Mar 29-Apr 4<\/h3>\n<ul>\n<li><strong>ADD<\/strong> Chatbot: tiered security model (T0-T3) with per-tool rate limits, T2 confirmation flow, T3 human approval queue + Filament panel<\/li>\n<li><strong>ADD<\/strong> Chatbot: <code>restart_device<\/code> (T2) and <code>update_device_settings<\/code> (T2) self-service tools<\/li>\n<li><strong>ADD<\/strong> Chatbot: <code>ToolAnalyticsService<\/code> for per-tool usage analytics via MongoDB aggregation<\/li>\n<li><strong>FIX<\/strong> Post directory: handle <code>?tags=string<\/code> query param without crashing (accept string, normalize to array in mount)<\/li>\n<li><strong>MOD<\/strong> Drop web sessions: remove <code>sessions<\/code> table dependency from SessionController, SessionsRelationManager, and DbLeanCommand inactive user cleanup<\/li>\n<li><strong>MOD<\/strong> Perf: nginx micro-cache (5s anonymous GET), static assets 30d, gzip level 6, 2048 worker connections, MySQL idle timeouts 300s, S3 preconnect, font subset, product media URLs 7d expiry, views count cached 30min<\/li>\n<li><strong>ADD<\/strong> Checkout: sticky mobile pay bar appears when main submit button scrolls out of view<\/li>\n<li><strong>ADD<\/strong> GetReplayTimelineAction: queries positions + events for a device in a time range, downsamples by resolution, merges into sorted timeline with bounding box<\/li>\n<li><strong>ADD<\/strong> ReplayController + <code>GET api\/device\/{device}\/replay<\/code> route for position replay feature<\/li>\n<li><strong>ADD<\/strong> OfferResource: Filament admin resource with store\/plan\/published\/expired filters, inline items repeater, and visit action<\/li>\n<li><strong>ADD<\/strong> TikTokStudioClient: session-cookie-based TikTok video upload bypassing official OAuth API<\/li>\n<li><strong>ADD<\/strong> SocialPostResource: download action for media files using signed S3 URLs<\/li>\n<li><strong>MOD<\/strong> SocialPostResource: disable Twitter, LinkedIn, and TikTok channels (not yet implemented)<\/li>\n<li><strong>MOD<\/strong> PublishSocialPostJob: default YouTube privacy to public, use post thumbnail as custom YouTube thumbnail<\/li>\n<li><strong>FIX<\/strong> Offer checkout: exclude free items from normal price calculation to fix discount amount<\/li>\n<li><strong>FIX<\/strong> Offer checkout: fix <code>Number::currency()<\/code> parameter from <code>maxPrecision<\/code> to <code>precision<\/code><\/li>\n<li><strong>MOD<\/strong> Offer show: simplify related offers resolver into single query with collection filtering<\/li>\n<li><strong>FIX<\/strong> SQS emails queue name mismatch causing <code>QueueDoesNotExist<\/code> on AI high-intent email dispatch<\/li>\n<li><strong>MOD<\/strong> Add lead ID and city to AI high-intent Slack log messages<\/li>\n<li><strong>MOD<\/strong> Merge setup-completed and reactivation notifications into a single <code>DeviceFirstPositionNotification<\/code> triggered on first position received<\/li>\n<li><strong>ADD<\/strong> FR translations for offer.show: &quot;Other offers&quot;, &quot;What our customers say&quot;, &quot;You save :amount&quot;<\/li>\n<\/ul>\n<h3>Week 4 \u00b7 Mar 22-28<\/h3>\n<ul>\n<li><strong>ADD<\/strong> EML Automobile client logo and move logos bar above testimonials as social proof block<\/li>\n<li><strong>ADD<\/strong> Automated lifecycle ad pipeline: <code>GenerateLifecycleAdListener<\/code> + <code>GenerateLifecycleAdJob<\/code> + Remotion carousel\/video compositions. On 90-day milestone, collects real device stats, generates Chatterbox TTS voiceover, renders 5-slide carousel + 30s video, schedules as social post via OmniPost<\/li>\n<li><strong>ADD<\/strong> OG image and YouTube thumbnail for AI chatbot blog post<\/li>\n<li><strong>ADD<\/strong> Minimal reading bar on blog post detail pages: replaces full header on scroll with back arrow, logo, truncated title, and article progress bar<\/li>\n<li><strong>ADD<\/strong> AI chatbot blog post (EN + FR) with chat mockup screenshots, real quote flow case study, OTP verification section, human handoff example, and technical deep-dive<\/li>\n<li><strong>ADD<\/strong> <code>LeadCreatedEvent<\/code> + Meta CAPI and Google Ads listeners: fire Lead conversion on form submit (not just email verification), giving ad algorithms more signal volume<\/li>\n<li><strong>MOD<\/strong> Migrate screenshot scripts from Puppeteer to Playwright, merge two screenshot skills into one<\/li>\n<li><strong>ADD<\/strong> <code>&lt;x-store.pricing-preframe&gt;<\/code> component: &quot;not the cheapest GPS tracker&quot; pre-frame on device.new, product show\/landing, and all industry pages<\/li>\n<li><strong>MOD<\/strong> Industry pages (show, taxi-vtc, car-rental): add pricing section, pricing pre-frame, and honest-limits to convert visitors, not just inform them<\/li>\n<li><strong>ADD<\/strong> <code>crm:process-bounces<\/code> artisan command: reads founder mailbox via IMAP, processes hard bounces (marks leads\/companies, records CRM activities, deletes bounce emails). Scheduled every 4h.<\/li>\n<li><strong>ADD<\/strong> <code>&lt;x-store.honest-limits&gt;<\/code> component: compact 3-column honest limitations on product landing + show pages<\/li>\n<li><strong>ADD<\/strong> <code>element\/page\/honest-limits.blade.php<\/code>: &quot;What Traxelio does NOT do&quot; damaging admissions section on landing page (4 honest limitations + FR translations)<\/li>\n<li><strong>ADD<\/strong> <code>solo.yml<\/code>: Solo desktop app config with Logs, Vite, Queue, Mail, Reverb (auto-start), Scheduler, Creativa, Ngrok, Tests, Lint, Analyze<\/li>\n<li><strong>MOD<\/strong> <code>view-conversation.blade.php<\/code>: redesign Filament conversation view as chat widget replica (header, bubbles, inline reply bar, markdown in bot messages)<\/li>\n<li><strong>ADD<\/strong> Fleet\/Enterprise pricing tier on both pricing pages (standalone + element partial): dark banner with volume discount, dedicated onboarding, SLA guarantee, &quot;Contact sales&quot; CTA + 13 FR translations<\/li>\n<li><strong>ADD<\/strong> <code>PlatformHealthService<\/code> + AiChatbot test suite (<code>phpunit.xml<\/code> + <code>composer.json<\/code> autoload-dev)<\/li>\n<li><strong>MOD<\/strong> <code>hormozi.md<\/code>: mark completed action items (close rates, guarantee badge, UTM audit, CAPI events, attribution report)<\/li>\n<li><strong>MOD<\/strong> <code>iot-sim-provider.md<\/code>: add Jimi IoT JM-VL02 Canada pilot details (V901 SIM carrier coverage, LTE bands, ISED status)<\/li>\n<li><strong>ADD<\/strong> Screenshot assets: ghost checkout slides, Jean conversation screenshots<\/li>\n<li><strong>ADD<\/strong> Lofi background music for spokesperson videos: 5 CC0 tracks in <code>public\/audios\/<\/code>, <code>SpokespersonLayout<\/code> plays selected track at 8% volume with fade-out<\/li>\n<li><strong>FIX<\/strong> Offline carousel slides: French accents across all 5 slides (fr\u00e9quentes, d\u00e9branch\u00e9, aliment\u00e9, r\u00e9seau, v\u00e9hicule, etc.)<\/li>\n<li><strong>ADD<\/strong> <code>ComparisonEnum<\/code>: 3DTechLogis case with full competitive data across all methods<\/li>\n<li><strong>MOD<\/strong> <code>ComparisonEnum<\/code>: replace all hardcoded USD price strings with <code>p(float)<\/code> helper using <code>CurrencyEnum::priceFromUsd()<\/code> for automatic XOF\/USD conversion<\/li>\n<li><strong>ADD<\/strong> <code>ComparisonEnum<\/code>: 6 new Dakar\/West Africa competitors (GPS My Tracker, Geo-Tracking, NatYaTrack, IT Mobile Afrique, Tracking Africa, Tramigo)<\/li>\n<li><strong>MOD<\/strong> <code>comparisons\/show.blade.php<\/code>: localize cost calculator with <code>Intl.NumberFormat<\/code> + visitor currency, fix stat card layout (grid tiles vs broken flex rows)<\/li>\n<li><strong>MOD<\/strong> <code>comparisons\/index.blade.php<\/code>: replace hardcoded $20 with <code>PlanEnum::PREMIUM-&gt;getPriceText()<\/code><\/li>\n<li><strong>MOD<\/strong> <code>competitors.md<\/code>: add 3DTechLogis + 8 Senegal-specific competitor entries<\/li>\n<li><strong>FIX<\/strong> <code>QuoteController<\/code>: add missing <code>Order<\/code> and <code>CurrencyEnum<\/code> imports causing 500 on quote show page<\/li>\n<li><strong>MOD<\/strong> <code>tasks\/todo.md<\/code>: add action plan items 13-18 from 3DTechLogis competitive analysis<\/li>\n<li><strong>MOD<\/strong> <code>ConversationResource<\/code>: add delete row action + bulk delete action<\/li>\n<li><strong>MOD<\/strong> <code>SocialPostResource<\/code>: media upload hidden on create, directory uses <code>social-posts\/{id}\/medias\/<\/code> on edit only<\/li>\n<li><strong>ADD<\/strong> <code>SocialPostObserver<\/code>: deletes S3 media files and directory when a post is deleted<\/li>\n<li><strong>FIX<\/strong> Ghost checkout Remotion video: French accents across all slide components and scene captions<\/li>\n<li><strong>FIX<\/strong> <code>QualifyLeadTool<\/code>: make phone optional so web-channel tool calls don't crash with null phone<\/li>\n<li><strong>FIX<\/strong> <code>ChatbotService<\/code>: guard against empty text response after tool loop exhaustion<\/li>\n<li><strong>MOD<\/strong> <code>neptune-chatbot.php<\/code>: raise default <code>max_tool_iterations<\/code> from 3 to 5<\/li>\n<li><strong>FIX<\/strong> <code>SearchKnowledgeTool<\/code>: use <code>summary<\/code> column instead of <code>content_sub<\/code><\/li>\n<li><strong>FIX<\/strong> <code>SystemPromptBuilder<\/code>: one tool call per turn for create_lead + qualify_lead sequence<\/li>\n<li><strong>ADD<\/strong> Premium guarantee badge on pricing pages: &quot;Device live within 4h or free on-site visit&quot; \u00b7 FR translation added<\/li>\n<li><strong>MOD<\/strong> <code>SubscriptionMilestoneCommand<\/code>: wire into scheduler at 07:00 daily \u00b7 add 8 FR translation keys for milestone notification<\/li>\n<li><strong>ADD<\/strong> Ghost checkout talking-head video asset + traxelio-checkout screenshot for Remotion spokesperson video<\/li>\n<li><strong>ADD<\/strong> <code>SubscriptionMilestoneCommand<\/code> + <code>SubscriptionMilestoneEvent<\/code> + <code>SendSubscriptionMilestoneNotificationListener<\/code>: day-90 check-in notification for subscriptions hitting their 3-month milestone<\/li>\n<li><strong>ADD<\/strong> <code>AnimatedSlideCard<\/code> component + <code>LiveSlideScene<\/code>: new spokesperson scene type for displaying animated slide cards in talking-head videos<\/li>\n<li><strong>MOD<\/strong> <code>GhostCheckoutCTASlide<\/code>: personalized CTA copy (&quot;Vous etiez si proche&quot;), larger body text, wider button padding, URL simplified to traxelio.com<\/li>\n<li><strong>ADD<\/strong> <code>Business-GhostCheckout-Video<\/code> Remotion composition registered in Root.tsx<\/li>\n<li><strong>MOD<\/strong> <code>docs\/insights\/hormozi.md<\/code>: remove completed exit-survey task<\/li>\n<li><strong>ADD<\/strong> <code>ChatbotCrossChannelIdentificationTest<\/code>: integration test verifying <code>VisitorContextResolver<\/code> identifies the same user via web fingerprint, user_id, SMS\/WhatsApp phone, and email; <code>Integration<\/code> testsuite added to phpunit.xml, excluded from normal runs<\/li>\n<li><strong>ADD<\/strong> <code>SendQuoteTool<\/code> + <code>CreateQuoteOrderAction<\/code>: chatbot can now generate a personalized fleet quote and return a shareable link; quote creation logic extracted into a reusable action shared with <code>QuoteController<\/code><\/li>\n<li><strong>ADD<\/strong> <code>SEARCH_KNOWLEDGE<\/code> + <code>SEND_QUOTE<\/code> cases to <code>ChatbotToolEnum<\/code>; <code>SEND_QUOTE<\/code> added to phone-injected tools<\/li>\n<li><strong>ADD<\/strong> <code>SearchKnowledgeTool<\/code>: unified fan-out search across all knowledge sources (posts, glossary, guides, industries, tracker devices) in a single tool call<\/li>\n<li><strong>ADD<\/strong> AI chatbot knowledge tools: <code>GetIndustryInfoTool<\/code>, <code>GetGlossaryTermTool<\/code>, <code>GetGuideTool<\/code>, <code>SearchPostsTool<\/code>, <code>GetTrackerInfoTool<\/code> \u2014 gives chatbot access to all site knowledge via dedicated tools<\/li>\n<li><strong>MOD<\/strong> <code>IndustryEnum::getBusinessContext()<\/code>: taxi-vtc driver payment models (Duo, Commission, Rental), business numbers for Dakar market; car-rental pricing context<\/li>\n<li><strong>ADD<\/strong> <code>Business-GhostCheckout<\/code> carousel (5 slides): PAS-framework product carousel using ghost checkout data as hook \u2014 ghost icon stat, buyer journey breakdown, captured data card, recovery steps, CTA<\/li>\n<li><strong>ADD<\/strong> <code>ChatbotService<\/code>: Slack notification on new conversation (channel, contact, locale, first message)<\/li>\n<li><strong>MOD<\/strong> <code>ChatbotService<\/code> + <code>SystemPromptBuilder<\/code>: pass resolved <code>VisitorContext<\/code> into system prompt on every message across all channels (web cookie, user_id, phone, email)<\/li>\n<li><strong>FIX<\/strong> <code>CheckoutController<\/code> + <code>DeviceNewController<\/code>: fall back to Visitor model UTMs when cookies are missing at checkout, fixing ~77% of unattributed device purchases<\/li>\n<li><strong>FIX<\/strong> <code>SubscriptionAttributionCommand<\/code>: fix revenue using order_items sum (no <code>total<\/code> column), add Visitor model enrichment for historical unattributed orders<\/li>\n<li><strong>ADD<\/strong> <code>VisitorActivityTypeEnum::CHAT_STARTED<\/code> + <code>ChatbotService<\/code>: record CRM visitor activity on first web chat contact, link conversation to visitor fingerprint<\/li>\n<li><strong>MOD<\/strong> <code>ChatWidget<\/code> + <code>ChatbotController<\/code> + <code>IncomingMessage<\/code>: pass visitor fingerprint through chatbot API for CRM linkage<\/li>\n<li><strong>ADD<\/strong> <code>lib\/crm\/src\/Services\/VisitorContextResolver<\/code> + <code>lib\/crm\/src\/DTO\/VisitorContext<\/code>: resolve visitor and lead context from fingerprint, user_id, phone, or email<\/li>\n<li><strong>MOD<\/strong> <code>SystemPromptBuilder<\/code>: inject visitor context (location, source, engagement, lead status, devices) into chatbot system prompt when available<\/li>\n<li><strong>FIX<\/strong> <code>job_batches<\/code> lock contention: route batch dispatches in <code>DeviceSetupJob<\/code>, <code>ExportJob<\/code>, <code>RecalculateVisitorScoresCommand<\/code> to dedicated <code>batches<\/code> queue (2 workers) instead of default (12 workers) \u00b7 add <code>queue-batches.conf<\/code> supervisor conf<\/li>\n<li><strong>FIX<\/strong> <code>ViewConversation<\/code>: extend <code>ViewRecord<\/code> instead of <code>Page<\/code> \u00b7 remove manual <code>mount()<\/code> and <code>$record<\/code> property (Filament handles hydration)<\/li>\n<li><strong>FIX<\/strong> <code>Conversation<\/code> model: remove explicit <code>$collection<\/code> override (MongoDB resolves collection name from class name automatically)<\/li>\n<li><strong>MOD<\/strong> pricing toggles (<code>element\/page\/pricing.blade.php<\/code>, <code>page\/pricing.blade.php<\/code>): active state uses brand color (<code>bg-primary-100 text-primary-700<\/code>) instead of hard black<\/li>\n<li><strong>MOD<\/strong> <code>docs\/operations\/daily.md<\/code>: add action items protocol \u2014 task list creation after each run, grouped by winback\/retention\/leads\/technical\/content<\/li>\n<li><strong>MOD<\/strong> <code>docs\/posts\/en\/how-we-recovered-a-lost-sale-visitor-intelligence.md<\/code>: numbered sections, add section 7 on OTP friction and WhatsApp\/magic-link alternatives (EN + FR)<\/li>\n<li><strong>ADD<\/strong> <code>public\/images\/og\/posts\/<\/code>: re-rendered OG images for both EN and FR visitor intelligence post \u00b7 added entries to <code>render-og.mjs<\/code><\/li>\n<li><strong>ADD<\/strong> <code>tasks\/screenshot.mjs<\/code> + <code>.claude\/skills\/screenshot.md<\/code>: general-purpose Puppeteer screenshot script saving to <code>screens\/web\/<\/code> or <code>screens\/mobile\/<\/code><\/li>\n<li><strong>ADD<\/strong> <code>tasks\/render-thumbnail.mjs<\/code> + <code>tasks\/thumbnail-templates\/<\/code> + <code>.claude\/skills\/thumbnail.md<\/code>: YouTube thumbnail renderer with <code>dark<\/code> (default) and <code>light<\/code> templates, props-driven via JSON<\/li>\n<li><strong>ADD<\/strong> <code>docs\/content\/youtube-thumbnail-guidelines.md<\/code>: data-backed YouTube thumbnail best practices (contrast, text limit, safe zones, CTR benchmarks)<\/li>\n<li><strong>ADD<\/strong> <code>public\/images\/screens\/web\/<\/code> + <code>public\/images\/screens\/mobile\/<\/code>: standardized screenshot output folders for web and mobile app captures<\/li>\n<li><strong>MOD<\/strong> <code>SubscriptionEndingNotification<\/code>: annual upsell for monthly subscribers across mail, push, and WhatsApp channels<\/li>\n<li><strong>MOD<\/strong> <code>element\/page\/pricing.blade.php<\/code>: default to annual billing with monthly\/annual toggle, show per-month equivalent and annual total<\/li>\n<li><strong>ADD<\/strong> <code>SubscriptionAttributionCommand<\/code> (<code>subscription:attribution<\/code>): weekly UTM attribution report grouping paid orders by source, campaign, and combination<\/li>\n<li><strong>MOD<\/strong> <code>SendMetaSubscribeOnRenewalListener<\/code>: extend CAPI Subscribe coverage to PRODUCT target orders (new device purchases)<\/li>\n<li><strong>MOD<\/strong> <code>SubscriptionUpgradedEvent<\/code>: add <code>clientIp<\/code> and <code>clientUserAgent<\/code> fields; pass through in <code>SendMetaSubscribeConversionListener<\/code><\/li>\n<li><strong>ADD<\/strong> <code>docs\/insights\/hormozi.md<\/code>: Hormozi video insights with prioritized revenue-impact action plan<\/li>\n<li><strong>FIX<\/strong> <code>tasks\/lead-digest.php<\/code>: remove legacy <code>score<\/code> fallback in hot leads query that was surfacing false positives<\/li>\n<li><strong>FIX<\/strong> <code>tasks\/visitor-daily.php<\/code>: correct <code>landing_page<\/code> \u2192 <code>entry_page<\/code> field name (was 100% unknown), add ratio-based view-to-cart funnel signal, fix day count float display<\/li>\n<li><strong>MOD<\/strong> <code>tasks\/customer-success.php<\/code>: split CRM coverage into actionable (has lead, no outreach) vs data gap (no lead record) groups<\/li>\n<li><strong>ADD<\/strong> <code>tasks\/expired-sub-outreach.php<\/code>: permanent script to surface expired subscriptions missing a founder winback email<\/li>\n<li><strong>MOD<\/strong> <code>docs\/operations\/daily.md<\/code>: reference new expired-sub-outreach script<\/li>\n<li><strong>ADD<\/strong> Astra Telematics full AT series to tracker device catalog (AT-110, AT-111, AT-202, AT-211, AT-241, AT-242, AT-300, AT-400, AT-402, AT-406, AT-500)<\/li>\n<li><strong>FIX<\/strong> Hide Apple Sign-In on Android devices (login + checkout) to prevent silent auth failures losing paid conversions<\/li>\n<li><strong>MOD<\/strong> Remove Sentry section from daily ops (no MCP tool available)<\/li>\n<li><strong>ADD<\/strong> <code>tasks\/customer-deep-dive.php<\/code>: reusable 360 customer analysis script (account, devices, settings, usage, unused features, CRM history)<\/li>\n<li><strong>ADD<\/strong> <code>tasks\/backfill-leads.php<\/code>: one-time script to create Lead records for paying users missing them<\/li>\n<li><strong>FIX<\/strong> Skip subscription ending\/terminated and document expiration notifications for disabled devices<\/li>\n<li><strong>ADD<\/strong> Sync consumable limits on subscription creation via SubscriptionObserver<\/li>\n<li><strong>ADD<\/strong> <code>tasks\/backfill-consumable-limits.php<\/code>: backfill per-entity limits for devices missing them<\/li>\n<li><strong>MOD<\/strong> <code>tasks\/usage-daily.php<\/code>: add per-feature breakdown with entity counts, averages, and progress bars<\/li>\n<li><strong>FIX<\/strong> CartIcon: remove <code>#[Lazy]<\/code> to prevent <code>PublicPropertyNotFoundException<\/code> when batched with other lazy components<\/li>\n<li><strong>ADD<\/strong> French translations for newsletter signup page (<code>\/newsletter\/new<\/code>)<\/li>\n<\/ul>\n<h3>Week 3 \u00b7 Mar 15-21<\/h3>\n<ul>\n<li><strong>FIX<\/strong> Google Ads: add Consent object (ad_user_data, ad_personalization) to enhanced conversion uploads to fix &quot;enhanced conversions for leads&quot; API error<\/li>\n<li><strong>ADD<\/strong> Usage tracking: wire remaining consumables (reports, geo zones, teams, shared accounts, SMS, exports, shared links) with <code>PeriodEnum<\/code>, <code>release()<\/code> for hard-cap consumables, and plan-based limits via <code>SyncConsumableLimitsAction<\/code><\/li>\n<li><strong>MOD<\/strong> AI chatbot: multi-store product tool with location-based store suggestion, industry\/guide context in system prompt, markdown-to-WhatsApp bold conversion, SMS markdown stripping, message history increased to 1000<\/li>\n<li><strong>MOD<\/strong> Chat widget: full-screen mobile layout, paginated message history with load-more, markdown rendering for assistant messages<\/li>\n<li><strong>ADD<\/strong> Facebook\/Instagram OAuth flow in omni-post package (routes, token exchange, page\/IG channel storage)<\/li>\n<li><strong>MOD<\/strong> Landing pages: app screenshot carousel on lead create and app-overview sections, &quot;View All Features&quot; changed to text link<\/li>\n<li><strong>MOD<\/strong> Deploy script: add PHP extension check for all required extensions<\/li>\n<li><strong>MOD<\/strong> Code review checklist: add usage tracking section<\/li>\n<li><strong>FIX<\/strong> LeadResource: handle array values in metadata KeyValue field<\/li>\n<li><strong>FIX<\/strong> <code>AppointmentResource<\/code>: add null-safe operator on device relationship to prevent crash when device is null<\/li>\n<li><strong>MOD<\/strong> Usage boost: display payment methods one per row, fix missing French accents in translations<\/li>\n<li><strong>FIX<\/strong> Order edit: &quot;View buyer&quot; hint action now uses buyer route key instead of raw ID<\/li>\n<li><strong>FIX<\/strong> Store product landing route: fix <code>Product::resolveRouteBinding<\/code> to check <code>$store instanceof Store<\/code> before scoping query, add <code>Route::model<\/code> binding via <code>marketplace.models.product<\/code> config so <code>{product}<\/code> always resolves to the app-level Product class<\/li>\n<li><strong>FIX<\/strong> Product landing: header and mobile sticky &quot;Order now&quot; buttons now submit the cart form directly instead of scrolling to an anchor<\/li>\n<li><strong>FIX<\/strong> Product checkout redirect: fix <code>App\\Models\\Product<\/code> type-hint, remove <code>product.checkout.form<\/code> from abandoned cart tracking and CRM config (only cart\/store checkout routes are tracked)<\/li>\n<li><strong>FIX<\/strong> <code>GeoZoneController::destroy<\/code>: rename <code>delete<\/code> to <code>destroy<\/code> to match Laravel <code>apiResource<\/code> convention (BadMethodCallException in production)<\/li>\n<li><strong>ADD<\/strong> Vehicle risk scoring in activity reports: 6-axis score (harsh events, speed discipline, impacts, night driving, mileage intensity, fatigue) with <code>ComputeVehicleRiskScoreAction<\/code>, <code>VehicleRiskScore<\/code> DTO, <code>RiskLevelEnum<\/code><\/li>\n<li><strong>MOD<\/strong> Merge HARD_ACCELERATION, HARD_BRAKING, HARD_CORNERING feature pages into DRIVING_BEHAVIORS with 301 redirects<\/li>\n<li><strong>MOD<\/strong> Remove RISK_REPORTS feature enum (risk scoring now lives inside activity reports)<\/li>\n<li><strong>FIX<\/strong> <code>CrmAnalyticsService::countVisits<\/code>: pass Carbon objects instead of strings to MongoDB date queries (earning per visit was always 0)<\/li>\n<li><strong>MOD<\/strong> Product landing: remove &quot;Sans abonnement obligatoire&quot; badge and recent orders toast social proof<\/li>\n<li><strong>FIX<\/strong> <code>Post::reading_time_text<\/code>: Carbon diffInMinutes always returned 0\/1, replaced with direct ceil(seconds\/60) math<\/li>\n<li><strong>MOD<\/strong> Testimonials: add Credit Kash and Soeco client logos (EN + FR)<\/li>\n<li><strong>ADD<\/strong> Product updates newsletter system: <code>ProductUpdateNotification<\/code>, <code>SendProductUpdateEmailJob<\/code>, <code>NewsletterController<\/code> with captcha, footer subscription form (Alpine.js), Filament &quot;Send as Newsletter&quot; action with incremental sending (1\/10\/100\/all), locale-aware post resolution via <code>language:<\/code> tags and <code>translated_post_id<\/code><\/li>\n<li><strong>ADD<\/strong> <code>EnsureLeadOnProductUpdatesFunnel<\/code> listener on <code>NewUserEvent<\/code> to auto-enroll new users<\/li>\n<li><strong>ADD<\/strong> <code>BackfillProductUpdatesFunnelCommand<\/code> to enroll existing leads and create leads for orphan users<\/li>\n<li><strong>ADD<\/strong> 4 product update newsletter editions (Jan, Feb, Mar W1, Mar W2) in EN + FR<\/li>\n<li><strong>FIX<\/strong> <code>AppointmentStatusCommand<\/code>: use <code>scheduled_at<\/code> instead of <code>created_at<\/code> to delete stale unconfirmed appointments, fix inverted comparison operator<\/li>\n<li><strong>FIX<\/strong> Tests: <code>AbandonedCartStatusCommandTest<\/code> \u2014 add <code>Queue::fake()<\/code> to suppress sync-queue side effects, fix time window and fulfilled-order assertions<\/li>\n<li><strong>FIX<\/strong> Tests: <code>AdminBusinessReportJobTest<\/code> \u2014 remove stale Insurance section assertion, update first-ReportSection index (6 \u2192 9) and New Visitors format to include bot count<\/li>\n<li><strong>FIX<\/strong> Tests: <code>DeviceStatusListenerTest<\/code> \u2014 skip <code>testBinding<\/code> since <code>DeviceStatusListener<\/code> is intentionally unregistered in EventServiceProvider<\/li>\n<li><strong>FIX<\/strong> Tests: <code>DeviceResourceTest<\/code> \u2014 use <code>COBAN_403<\/code> model so <code>DeviceObserver<\/code> sets <code>can_arm_alarm = true<\/code> instead of being overwritten to false<\/li>\n<\/ul>\n<h3>Week 2 \u00b7 Mar 8-14<\/h3>\n<ul>\n<li><strong>ADD<\/strong> Store: product pricing tiers, upsell flow, appointment scheduling, alternative pages, CEO dashboard widget, reviews relation manager<\/li>\n<li><strong>FIX<\/strong> Order show: guard upsell block behind <code>$isBuyer<\/code>, use <code>paid_at<\/code> instead of removed <code>is_paid<\/code> accessor<\/li>\n<li><strong>FIX<\/strong> <code>DeliveryFactory<\/code>: null-safe <code>$after<\/code> for <code>scheduled_before<\/code> to prevent clone-on-null crash<\/li>\n<li><strong>FIX<\/strong> <code>ProcessWaveEventJob<\/code>: re-enable webhook timestamp validation (was commented out)<\/li>\n<li><strong>MOD<\/strong> <code>RecordPurchaseCompletedActivityListener<\/code>: link visitor to lead on purchase via <code>LinkVisitorToLeadJob<\/code><\/li>\n<li><strong>MOD<\/strong> <code>AlertOnHotVisitorListener<\/code>: always alert to Slack (remove conditional)<\/li>\n<li><strong>MOD<\/strong> <code>SendAbandonedCartSequenceListener<\/code>: type-safe <code>User<\/code> instanceof check on polymorphic buyer<\/li>\n<li><strong>MOD<\/strong> Test suite: fix factories, mock bindings, and assertion updates across 15+ test files<\/li>\n<li><strong>FIX<\/strong> Filament Select: add null-coalescing fallbacks to all <code>getOptionLabelUsing<\/code> callbacks across 12 resource files to prevent crash when referenced model is deleted or has null name<\/li>\n<li><strong>FIX<\/strong> <code>LogoutAction<\/code>: null-safe access token delete to prevent crash when logging out via web session (no Sanctum token)<\/li>\n<li><strong>ADD<\/strong> <code>docs\/posts\/fr\/comparatif-traceur-gps-flotte-dakar.md<\/code>: new French hub post targeting &quot;comparatif traceur GPS flotte&quot; (pos 38) and &quot;comparer traceur GPS flotte&quot; (pos 18) with comparison table, pricing in FCFA, and sector-by-sector guide<\/li>\n<li><strong>MOD<\/strong> <code>PostSeeder<\/code>: register new hub post + fix <code>outils-suivi-flotte-dakar<\/code> title to &quot;Gestion de flotte a Dakar&quot; to match query intent (was a question format losing to the blog index)<\/li>\n<li><strong>ADD<\/strong> <code>config\/traxelio.php<\/code> + <code>routes\/redirects.php<\/code>: 301 redirect <code>comparatif-geolocalisation-flotte-dakar<\/code> (and ID variant) to new hub to consolidate split signal<\/li>\n<li><strong>MOD<\/strong> Internal links: add hub and <code>outils-suivi-flotte-dakar<\/code> backlinks from <code>optimiser-gestion-flotte-automobile-gps<\/code> and <code>prix-gps-flotte-senegal-guide-complet<\/code><\/li>\n<li><strong>ADD<\/strong> <code>ComparisonEnum<\/code>: Fleeti, Cartrack, MiX Telematics comparison pages targeting African\/francophone competitors<\/li>\n<li><strong>MOD<\/strong> <code>ComparisonEnum<\/code>: Motive + Geotab meta rewritten with pricing signals and ELD-specific FAQs to fix 0-click at position 3-4<\/li>\n<li><strong>ADD<\/strong> <code>routes\/redirects.php<\/code>: 301 redirects for 4 Geotab blog posts to <code>\/compare\/traxelio-vs-geotab<\/code> to consolidate link equity<\/li>\n<li><strong>ADD<\/strong> <code>lang\/fr.json<\/code>: French translations for location page meta and comparison index to fix English meta showing to French-locale users<\/li>\n<li><strong>MOD<\/strong> Lead qualification flow: trim to 3 steps (industry, operating area, fleet size), remove usage\/business-info\/benefits\/after-hours\/more-vehicles\/skip, rebalance scoring to 100pts across 3 signals<\/li>\n<li><strong>ADD<\/strong> <code>OperatingAreaEnum<\/code>, <code>FleetSizeEnum<\/code> with <code>HasLabel<\/code>+<code>HasDescription<\/code>; use <code>Rule::enum()<\/code> in controller; use enums in <code>LeadResource<\/code> form, <code>LeadBucketEnum<\/code>, and <code>Lead::calculateScore()<\/code><\/li>\n<li><strong>MOD<\/strong> Qualify stepper: center breadcrumb, reduce to 3 steps, remove skip button<\/li>\n<li><strong>MOD<\/strong> Operating area + fleet size qualify steps: card-style layout with per-option SVG illustrations<\/li>\n<li><strong>FIX<\/strong> <code>ImpactDetectionJob<\/code>: fix crash loop caused by <code>CreateAccelerationJob<\/code> dispatching with old single-arg signature after refactor \u2014 now passes <code>deviceId<\/code>, <code>positionId<\/code>, <code>previousPositionId<\/code> from the acceleration record<\/li>\n<li><strong>FIX<\/strong> <code>SubscriptionExtendedNotification<\/code>: cast <code>ends_at<\/code> Carbon to string before passing to notification constructor to fix type error on subscription extension<\/li>\n<li><strong>FIX<\/strong> <code>PostResource<\/code>: widen <code>getSearchResultsUsing<\/code> closure type hint from <code>?FilamentPost<\/code> to <code>?Post<\/code> to fix Filament passing base model instance<\/li>\n<li><strong>FIX<\/strong> <code>DatabaseSeeder<\/code>: scope <code>Fake\\DatabaseSeeder<\/code> to <code>local<\/code> env only (was <code>! production<\/code>) to prevent FK violations on demo DB on every deploy<\/li>\n<li><strong>ADD<\/strong> <code>SendDeviceSmsCommandJob<\/code>: idempotency per phone+command via 24h cache key, sleep 10s after send, verify Twilio delivery status before proceeding to next command in chain<\/li>\n<\/ul>\n<h3>Week 1 \u00b7 Mar 1-7<\/h3>\n<ul>\n<li><strong>FIX<\/strong> Cart not cleared on payment abandonment: defer cart clearing until payment is confirmed. Store <code>cart_item_ids<\/code> in <code>order-&gt;data<\/code> at checkout, clear via <code>ClearCartOnPaymentCompletedListener<\/code> on <code>PaymentCompletedEvent<\/code> (online payments) and inline in <code>CashOnDeliveryController::verify()<\/code> (CoD OTP confirmation)<\/li>\n<li><strong>ADD<\/strong> Visitor Globe: funny nicknames (deterministic adjective+noun) and DiceBear avatars on markers and feed items<\/li>\n<li><strong>ADD<\/strong> Visitor Globe: event type filter chips (All, Conversions, Page views, GPS devices, API) on activity feed sidebar<\/li>\n<li><strong>ADD<\/strong> Visitor Globe: anonymized GPS device positions broadcast on public channel with jittered coordinates, blue dots, and device counter<\/li>\n<li><strong>ADD<\/strong> Visitor Globe: purchase prediction score (0-100) with lazy-loaded progress bar on marker click, based on engagement, conversion signals, recency, and depth<\/li>\n<li><strong>MOD<\/strong> Visitor Globe: make activity feed sidebar collapsible on mobile as a right drawer with hamburger toggle, responsive stats overlay<\/li>\n<li><strong>MOD<\/strong> Visitor Globe: interactive globe with drag-to-rotate, click markers for detail card, click feed items to focus\/rotate globe, hover pauses rotation<\/li>\n<li><strong>FIX<\/strong> Visitor Globe: fix duplicate GPS device events with backend cache throttle (30s\/device) and frontend dedup (25s\/nickname)<\/li>\n<li><strong>FIX<\/strong> Visitor Globe: fix Alpine.js &quot;duplicate key&quot; crash by assigning unique IDs to all markers<\/li>\n<li><strong>MOD<\/strong> Visitor Globe: auto-cleanup events older than 10min to free browser memory, show GPS coordinates in device feed items<\/li>\n<li><strong>ADD<\/strong> Visitor Globe: light\/dark\/system theme toggle with CSS custom properties, globe adapts colors per theme, persisted in localStorage<\/li>\n<li><strong>MOD<\/strong> Visitor Globe: clicking sidebar item on mobile closes drawer while keeping map popup visible<\/li>\n<li><strong>MOD<\/strong> Visitor Globe: distinctive diamond-shaped GPS device markers with pulsing ring animation (vs round visitor dots)<\/li>\n<li><strong>FIX<\/strong> Visitor Globe: popup card positioning now tracks marker across globe rotation via rAF loop, centers card when marker is on back side<\/li>\n<li><strong>ADD<\/strong> Visitor Globe: zoom support via scroll wheel, double-click, and pinch-to-zoom with smooth interpolation and +\/-\/reset controls<\/li>\n<li><strong>MOD<\/strong> Visitor Globe: reduce GPS jitter from +-1km to +-220m for more accurate device positioning on globe<\/li>\n<li><strong>ADD<\/strong> Visitor Globe: tabbed sidebar with Activity, Visitors (sorted by engagement), and Devices (sorted by newest) tabs with count badges<\/li>\n<li><strong>FIX<\/strong> <code>NotifyForTimestampAnomaly<\/code>: suppress email notifications for <code>BUFFERED_BATCH<\/code> and <code>OFFLINE<\/code> scenarios \u2014 both are non-actionable noise (buffered reconnect flush and offline already handled by dedicated pipeline)<\/li>\n<li><strong>ADD<\/strong> Driver Fatigue Detection: scheduled command (<code>driver:fatigue-compute<\/code>) every 30 min computes composite fatigue score (0-100) from continuous driving duration, time-of-day risk, and speed variance. Stores results in <code>driver_fatigue_scores<\/code> table. Alerts via push + email at Warning (60) and Critical (80) thresholds with rate limiting. Toggleable per-device premium setting with custom threshold support. Mobile app settings screen with simple toggle<\/li>\n<li><strong>MOD<\/strong> Merge product checkout into cart checkout: consolidate two separate checkout flows (direct product + cart-based) into a single per-store cart checkout. Cart is global, checkout is per-store. Product landing &quot;Order now&quot; adds to cart and redirects to <code>cart.checkout.form?store=X<\/code>. COD OTP flow moved to <code>Order\/CashOnDeliveryController<\/code> at <code>\/order\/{order}\/cod<\/code>. Old product checkout routes redirect to product landing pages. Cart show groups items by store with per-store checkout buttons<\/li>\n<li><strong>FIX<\/strong> <code>AbandonedCartStatusCommand<\/code>: add logging (order count + per-order event) for debugging abandoned cart pipeline<\/li>\n<li><strong>ADD<\/strong> WhatsApp order lifecycle notifications: <code>SendWhatsAppOrderConfirmedListener<\/code> (fires on <code>PaymentCompletedEvent<\/code> for STORE_GPS orders), <code>OrderObserver<\/code> watching <code>ready_at<\/code> (out for delivery) and <code>delivered_at<\/code> (delivered) field changes<\/li>\n<li><strong>ADD<\/strong> <code>OrderConfirmedWhatsAppNotification<\/code>, <code>OrderOutForDeliveryWhatsAppNotification<\/code>, <code>OrderDeliveredWhatsAppNotification<\/code>: WhatsApp messages for full order lifecycle<\/li>\n<li><strong>ADD<\/strong> <code>AbandonedCartWhatsAppNotification<\/code>: immediate WhatsApp touchpoint (before email batch) when cart is abandoned; resolves checkout URL from order items<\/li>\n<li><strong>MOD<\/strong> <code>SendAbandonedCartSequenceListener<\/code>: send immediate WhatsApp to buyer (if phone set) before dispatching email batch<\/li>\n<li><strong>MOD<\/strong> <code>AbandonedCartStatusCommand<\/code> + <code>bootstrap\/app.php<\/code>: change schedule from hourly to every 15 minutes; shrink detection window from 1-2h to 15-30 min after cart creation<\/li>\n<li><strong>ADD<\/strong> Cash on Delivery (COD) payment flow: CASH method allowed in checkout, phone required for COD, COD branch in <code>CheckoutController<\/code> skips payment gateway, stores <code>cod_status<\/code> + <code>cod_expires_at<\/code> in order data, sends WhatsApp OTP confirmation request to buyer<\/li>\n<li><strong>ADD<\/strong> <code>CodConfirmationWhatsAppNotification<\/code>: OTP confirmation request with address and 2-hour expiry warning<\/li>\n<li><strong>ADD<\/strong> <code>checkout-cod-pending.blade.php<\/code>: confirmation page shown after COD order placement, instructs buyer to reply OUI on WhatsApp<\/li>\n<li><strong>ADD<\/strong> WhatsApp incoming webhook: <code>WhatsAppWebhookController<\/code> handles Meta verification (GET) and incoming messages (POST); OUI reply confirms COD order, notifies seller, replies to buyer<\/li>\n<li><strong>ADD<\/strong> <code>CancelUnconfirmedCodOrdersCommand<\/code> (<code>cod:cancel-expired<\/code>): deletes COD orders where <code>cod_status = pending_confirmation<\/code> and expiry has passed; runs hourly via scheduler<\/li>\n<li><strong>MOD<\/strong> <code>AdminBusinessReportJob<\/code>: add COD stats (pending, confirmed, confirmation rate) to Commerce section<\/li>\n<li><strong>MOD<\/strong> <code>PaymentMethodEnum<\/code>: add <code>codCases()<\/code> static helper returning <code>[CASH]<\/code><\/li>\n<li><strong>MOD<\/strong> <code>CheckoutRequest<\/code>: allow CASH in payment_method via <code>codCases()<\/code>; phone becomes required when payment_method is CASH<\/li>\n<li><strong>ADD<\/strong> <code>cod_enabled<\/code> column on <code>stores<\/code> table: COD is now a per-store opt-in flag (default off); <code>CheckoutController<\/code> hides CASH option when disabled, <code>CheckoutRequest<\/code> rejects CASH submissions server-side, <code>StoreResource<\/code> exposes the toggle in Filament admin<\/li>\n<li><strong>FIX<\/strong> <code>PaymentMethodEnum::fromCurrency(XOF)<\/code>: include <code>codCases()<\/code> so CASH appears as a valid XOF method (gated by store <code>cod_enabled<\/code>)<\/li>\n<li><strong>FIX<\/strong> <code>CheckoutController<\/code> <code>error_url<\/code>: point to <code>store.product.checkout.form<\/code> (was <code>store.product.show<\/code>)<\/li>\n<li><strong>MOD<\/strong> Unify checkout success into <code>order.show<\/code>: migrate celebration banner, appointment booking CTA, contextual upsells (premium + yearly), coupon generation, and Plausible revenue event from standalone checkout-success page into the permanent order show page<\/li>\n<li><strong>ADD<\/strong> <code>Order\/AppointmentController<\/code>: order-scoped appointment booking (replaces store\/product-scoped flow)<\/li>\n<li><strong>ADD<\/strong> <code>OrderControllerTest<\/code>: 8 tests covering upsell logic, coupon generation, celebration banner, and appointment CTA<\/li>\n<li><strong>ADD<\/strong> Outbound Logistics &amp; Delivery: 5-email cold sequence (Intro, ValueDrop, Story, Nudge, Breakup) with <code>FunnelEnum::OUTBOUND_LOGISTICS_DELIVERY<\/code>, wired via <code>IndustryEnum::getOutboundFunnel()<\/code> and registered in <code>DispatchOutboundSequenceAction<\/code><\/li>\n<li><strong>ADD<\/strong> Dynamic pricing helpers on <code>OutboundNotification<\/code>: <code>trackerPrice()<\/code>, <code>planPrice()<\/code>, <code>leadCity()<\/code> resolve currency from lead metadata for localized pricing in outbound emails<\/li>\n<li><strong>FIX<\/strong> <code>OutboundNotification::companyName()<\/code>: read <code>metadata.company<\/code> (Filament form field) with fallback to <code>metadata.company_name<\/code><\/li>\n<li><strong>MOD<\/strong> Retrofit CarRental and Dealership breakup emails to use dynamic <code>trackerPrice()<\/code>\/<code>planPrice()<\/code> instead of hardcoded &quot;CA$89&quot; \/ &quot;CA$13\/month&quot;<\/li>\n<li><strong>ADD<\/strong> CRM visitor merging: <code>MergeVisitorsAction<\/code> deduplicates visitor records across sessions, <code>MergeVisitorsCommand<\/code> for CLI usage, <code>RecordVisitorActivityJob<\/code> links visitors to leads via email tracking<\/li>\n<li><strong>ADD<\/strong> CRM bounce tracking: <code>SkipIfBounced<\/code> and <code>RateLimitPerFunnel<\/code> job middlewares for email sequences, <code>HasEmailBounceTracking<\/code> model concern<\/li>\n<li><strong>MOD<\/strong> CRM bot detection: refactor <code>UserAgentParser<\/code> with structured <code>UserAgentResult<\/code>, improve bot probability scoring<\/li>\n<li><strong>ADD<\/strong> Outbound sales sequences for 5 industry verticals: Construction, FieldService, LogisticsDelivery, SmallBusinessFleet, TaxiVtc (each with full 6-email sequence)<\/li>\n<li><strong>MOD<\/strong> Replace <code>DispatchOutboundDealershipCommand<\/code> with generalized <code>DispatchOutboundSequenceAction<\/code> supporting all industry verticals<\/li>\n<li><strong>ADD<\/strong> Auth refactor: extract <code>CreateApiTokenAction<\/code> and <code>DemoLoginAction<\/code>, add <code>SessionsRelationManager<\/code> for web session visibility in Filament, improve Apple\/Google social login error handling<\/li>\n<li><strong>ADD<\/strong> Tracker timestamp anomaly detection: <code>TrackerTimestampStatusCommand<\/code>, <code>DeviceTimestampAnomalyEvent<\/code>, <code>TimestampAnomalyNotification<\/code> for stale\/future timestamp alerts<\/li>\n<li><strong>ADD<\/strong> MTA-STS email security config, MongoDB\/MySQL\/Redis production config files<\/li>\n<li><strong>ADD<\/strong> Admin visitor globe page with <code>VisitorGlobeController<\/code><\/li>\n<li><strong>MOD<\/strong> Creativa: extract shared components (ContactFooter, EndCard, HeadlineBlock, LogoBar, PriceCard, VehicleTypeRow, MetricCards, TraxelioLogo, DecorativePin), reorganize coban303 ads into image\/ and video\/ subdirectories, add 6 new static image ad compositions<\/li>\n<li><strong>ADD<\/strong> FAQ system: extract <code>FaqContent<\/code> DTO with <code>FaqCategory<\/code> and <code>FaqItem<\/code> for structured FAQ rendering<\/li>\n<li><strong>MOD<\/strong> Captcha: configurable score thresholds in <code>CaptchaClient<\/code><\/li>\n<li><strong>ADD<\/strong> <code>EmailActivityTable<\/code> Filament concern for lead\/company email activity tracking<\/li>\n<li><strong>FIX<\/strong> <code>add_bundle_id_to_cart_items<\/code> migration: guard with <code>hasTable<\/code> check for environments without cart_items table<\/li>\n<\/ul>\n<h2>2026-02<\/h2>\n<h3>Week 4 \u00b7 Feb 22-28<\/h3>\n<ul>\n<li><strong>MOD<\/strong> GPS tracker market research: revised for Traxelio-native fleet hardware (SinoTrack ST-906L, Concox JM-VL04, Teltonika FMC130, Coban TK306A, Concox JM-LL301), cost breakdowns for 10 and 100 unit test orders, SIM card strategy, mixed fleet kit recommendations<\/li>\n<li><strong>ADD<\/strong> Dedicated CRM queue: move CRM\/MongoDB jobs (<code>RecordVisitorActivityJob<\/code>, <code>EnrichVisitorGeoJob<\/code>, <code>RecalculateVisitorScoreJob<\/code>, <code>LinkVisitorToLeadJob<\/code>) to separate SQS queue <code>traxelio-production-crm<\/code> with 4 dedicated supervisor workers. Refactor CRM <code>Job<\/code> base class to extend <code>App\\Jobs\\Job<\/code><\/li>\n<li><strong>FIX<\/strong> MongoDB OOM: cap WiredTiger cache to 512MB on production server<\/li>\n<li><strong>FIX<\/strong> Livewire <code>cart-icon<\/code> component: wrap conditional root tags in single <code>&lt;span&gt;<\/code> to fix <code>RootTagMissingFromViewException<\/code><\/li>\n<li><strong>FIX<\/strong> Rollback Livewire rate limiting: remove throttle middleware from <code>setUpdateRoute<\/code>, remove <code>livewire-update<\/code> rate limiter. Keep <code>livewire-scan<\/code> fail2ban, Makefile deploy-order fix, and <code>RootTagMissingFromViewException<\/code> suppression<\/li>\n<li><strong>FIX<\/strong> <code>SessionController<\/code>: guard <code>$currentToken-&gt;id<\/code> access against null when token is absent<\/li>\n<li><strong>ADD<\/strong> <code>ProductLandingController<\/code>: extract product landing page logic from <code>ProductController<\/code><\/li>\n<li><strong>MOD<\/strong> Checkout email validation: replace <code>required|email<\/code> with <code>EmailRule<\/code> (deep MX\/SMTP validation) in both <code>CheckoutRequest<\/code> classes<\/li>\n<li><strong>MOD<\/strong> <code>EmailRule<\/code>: add Slack logging for rejected emails, add <code>email_deep_validation<\/code> config toggle<\/li>\n<li><strong>MOD<\/strong> <code>CheckoutPageAbandonedNotification<\/code>: resolve product slug from visitor activity and pass to email CTAs for product-specific landing page links<\/li>\n<li><strong>FIX<\/strong> fail2ban jail logpaths: update from stale <code>releases\/current\/<\/code> path to <code>\/var\/www\/vhosts\/traxelio.com\/storage\/logs\/access.log<\/code><\/li>\n<li><strong>FIX<\/strong> Docker supervisor <code>queue-emails<\/code>: use SQS queue name <code>traxelio-production-emails<\/code> instead of <code>emails<\/code><\/li>\n<li><strong>FIX<\/strong> Store product landing sidebar: add <code>w-full<\/code> to checkout card for proper width<\/li>\n<li><strong>FIX<\/strong> <code>HasSelfHealingRoute<\/code>: extract overridable <code>getShowRouteName()<\/code> method; override in <code>LegacyProduct<\/code> to return <code>product.show<\/code> (fixes production <code>RouteNotFoundException<\/code>)<\/li>\n<li><strong>FIX<\/strong> Checkout bundle validation: add <code>exists<\/code> rule for <code>bundle_id<\/code> scoped to the product; add stock validation for combo bundle item products<\/li>\n<li><strong>FIX<\/strong> <code>AdminBusinessReportJob<\/code>: rename <code>$revenue<\/code> to <code>$orderRevenue<\/code>; fix <code>$inPreparation<\/code> filter to use <code>preparing_at<\/code>; use Carbon dates in MySQL queries; remove taxi\/bcdmotors section; add <code>StatSection<\/code> DTO<\/li>\n<li><strong>FIX<\/strong> <code>ProcessPositionsBatchJob<\/code>: wrap <code>Carbon::parse($fixTime)<\/code> in try\/catch to skip malformed timestamps<\/li>\n<li><strong>FIX<\/strong> FAQ JSON-LD: use <code>$displayed<\/code> instead of <code>$questions<\/code> so structured data matches rendered content<\/li>\n<li><strong>FIX<\/strong> <code>CartIcon<\/code> placeholder: reserve icon space with explicit dimensions to prevent CLS<\/li>\n<li><strong>FIX<\/strong> Tawk widget: use spread operator for <code>routeIs()<\/code> exclude list<\/li>\n<li><strong>FIX<\/strong> PHPStan: <code>@method<\/code> annotations on <code>Order<\/code>, MongoDB raw result type in <code>CrmAnalyticsService<\/code>, <code>HasActivities<\/code> annotation in <code>EmailTrackingController<\/code>, <code>HasLabel<\/code> check in <code>OrderResource<\/code><\/li>\n<li><strong>MOD<\/strong> Move post redirects from <code>PostSeeder::$redirects<\/code> to <code>config(&quot;traxelio.post_redirects&quot;)<\/code><\/li>\n<li><strong>MOD<\/strong> Cache featured reviews query in <code>ProductController<\/code> (15 min TTL)<\/li>\n<li><strong>MOD<\/strong> <code>StoreSelectController<\/code>: replace request mutation with direct <code>to_route()<\/code> redirects<\/li>\n<li><strong>MOD<\/strong> <code>config\/filesystems.php<\/code>: add empty-string defaults to <code>env('AWS_ROOT')<\/code> calls<\/li>\n<li><strong>MOD<\/strong> <code>LegacyProductResource<\/code>: add retirement comment explaining belt-and-suspenders gating<\/li>\n<li><strong>MOD<\/strong> <code>user\/profile<\/code>: replace inline button styles with <code>&lt;x-button.secondary&gt;<\/code> component across all action buttons; rename &quot;Download&quot; CTA to &quot;Open mobile app&quot;; add FR translation<\/li>\n<li><strong>ADD<\/strong> <code>StoreSelectController<\/code> + <code>store\/select<\/code> route: replaces generic <code>store.index<\/code> redirect with a focused radio-card store-selection page (GeoIP suggestion, auto-skip when single store). <code>product.start<\/code> now 302-redirects to <code>store.select<\/code> for backwards compatibility with old email links. Updated 9 notification files and 3 views to use <code>store.select<\/code>.<\/li>\n<li><strong>FIX<\/strong> SqsException: remove legacy <code>laravel-worker.conf<\/code> (old CF-stack workers targeting deleted queues); add <code>queue-default.conf<\/code> for bare-metal SQS default-queue workers; fix <code>queue:monitor<\/code> command to use <code>sqs:<\/code> prefix; fix healthcheck <code>QUEUE_LIST<\/code> to reflect actual SQS\/Redis split (<code>default<\/code>, <code>updates<\/code>, <code>positions<\/code> on SQS, <code>emails<\/code> on Redis)<\/li>\n<li><strong>FIX<\/strong> <code>DeviceActivationNotification<\/code>: CTA button for &quot;Pick GPS device&quot; was generating a broken URL (<code>\/store\/\/product<\/code>) due to missing store parameter; link now routes to <code>device.new.store-select<\/code> which auto-skips when only one store exists<\/li>\n<li><strong>MOD<\/strong> Makefile + <code>devops\/deploy.sh<\/code>: migrate deployment base paths from <code>\/var\/www\/vhosts\/traxelio.com\/<\/code> to <code>\/opt\/traxelio.com\/<\/code> (<code>releases\/<\/code>, <code>shared\/<\/code>); update healthcheck script path to <code>\/opt\/healthcheck.sh<\/code>; update logrotate and <code>health_check()<\/code> heredoc to use new shared dir<\/li>\n<li><strong>MOD<\/strong> Docker supervisor configs: update all artisan paths from <code>\/home\/gps\/www<\/code> to <code>\/var\/www\/vhosts\/traxelio.com<\/code>; rename <code>positions<\/code> \u2192 <code>queue-positions<\/code> and <code>queue-emails<\/code> \u2192 <code>queue-redis-emails<\/code>; remove legacy <code>laravel-worker.conf<\/code><\/li>\n<li><strong>MOD<\/strong> Docker nginx config: increase <code>gzip_comp_level<\/code> to 9, redirect logs to <code>storage\/logs\/<\/code>, add Livewire location block<\/li>\n<li><strong>MOD<\/strong> README: remove hardcoded credentials and infra secrets; replace with links to <code>docs\/setup.md<\/code> and <code>docs\/credentials.md<\/code><\/li>\n<li><strong>MOD<\/strong> Rename <code>SellerSeeder<\/code> \u2192 <code>StoreSeeder<\/code><\/li>\n<li><strong>ADD<\/strong> <code>devops\/supervisor\/conf.d\/queue-redis-default.conf<\/code>: production bare-metal supervisor conf for Redis <code>default<\/code> queue worker (user=gps, numprocs=2); mirrors Docker equivalent; resolves stuck abandoned-cart jobs dispatched to the wrong queue<\/li>\n<li><strong>FIX<\/strong> <code>SendAbandonedCartSequenceListener<\/code> and <code>SendCheckoutPageAbandonedSequenceListener<\/code>: add <code>.onQueue(config(&quot;queue.emails&quot;))<\/code> to all batched jobs and the batch dispatch, and use <code>$unit<\/code> for dynamic day\/min delay; previously jobs landed in <code>queues:default<\/code> Redis with no worker consuming them<\/li>\n<li><strong>MOD<\/strong> User profile: move &quot;Open web app&quot; from hero header button to a dedicated card below &quot;Download Traxelio App&quot;, with icon, title, and description for better visual hierarchy<\/li>\n<li><strong>FIX<\/strong> <code>LoginAction::ensureCanAccessDashboard()<\/code>: skip Filament panel access check when <code>Filament::getCurrentPanel()<\/code> returns null (public website routes), so regular users can log in via <code>\/login<\/code> without being blocked; check still enforced on Filament panel routes (<code>\/dashboard\/login<\/code>)<\/li>\n<li><strong>MOD<\/strong> User profile: remove &quot;Edit Profile&quot; button (Filament admin deep-link, inappropriate for non-admin users); add &quot;Back Office&quot; card section visible only to users with <code>canAccessDashboard()<\/code>, linking to the Filament dashboard<\/li>\n<li><strong>ADD<\/strong> <code>DemoInviteNotification<\/code>: new sales sequence email at day 17 inviting leads to a live demo; shift <code>MarkLeadAsLostJob<\/code> from day 15 to day 18; update <code>AddLeadToSalesFunnelTest<\/code> assertions for 7-job batch<\/li>\n<li><strong>FIX<\/strong> <code>CRMServiceProvider<\/code>: move <code>mergeConfigFrom<\/code> from <code>boot()<\/code> to <code>register()<\/code> so <code>neptune-crm<\/code> config is available at registration time, fixing null-config crashes in <code>VisitorScorer<\/code> and <code>SendSequenceEmailJob<\/code> in production<\/li>\n<li><strong>ADD<\/strong> Demo page: 3 app screenshots (live tracking, trip history, remote immobilizer) alongside the video, with feature label and description; FR translations added for all new strings<\/li>\n<li><strong>ADD<\/strong> <code>VisitorResource<\/code>: default <code>exclude_bots<\/code> filter (<code>bot_probability &lt; 50<\/code>) so the table and all 6 widgets show human-only traffic by default; toggleable from the filter panel to inspect bot traffic<\/li>\n<li><strong>ADD<\/strong> Car rental outbound funnel: <code>OUTBOUND_CAR_RENTAL<\/code> funnel enum with stages; refactor <code>DispatchOutboundSequenceAction<\/code> to be data-driven (single action supports both dealership and car rental sequences via <code>$funnel<\/code> param); fix <code>DispatchOutboundDealershipCommand<\/code> to pass explicit funnel; add <code>CarRental<\/code> notification stubs<\/li>\n<li><strong>ADD<\/strong> &quot;Open web app&quot; button on user profile: <code>UserController::openApp()<\/code> generates a Sanctum token with geo metadata and redirects to <code>app.traxelio.com\/auth<\/code><\/li>\n<li><strong>MOD<\/strong> Remove <code>screen_resolution<\/code> from visitor tracking: dropped from <code>TrackVisitorMiddleware<\/code>, <code>RecordVisitorActivityJob<\/code>, <code>HasViews<\/code>, <code>VisitorResource<\/code>, <code>VisitorFactory<\/code>, and <code>FakeVisitorDataSeeder<\/code><\/li>\n<li><strong>FIX<\/strong> <code>Visitor::calculateBotProbability()<\/code>: remove zero-engagement signal (+15) and no-language signal (+5) \u2014 both were structural false positives hitting 60-100% of real visitors; also remove <code>cloudflare<\/code> from <code>DATACENTER_PATTERNS<\/code> (Cloudflare is CDN\/corporate infra, not datacenter-bot indicator); max score for a non-crawler, non-datacenter visitor now correctly tops at 25 (Unknown device + Unknown browser); add <code>VisitorBotProbabilityTest<\/code> with 13 regression-guard test cases<\/li>\n<li><strong>FIX<\/strong> Marketing notification <code>toMail()<\/code> methods: remove <code>App\\Models\\Lead<\/code> type hint from all 39 notifications under <code>app\/Notifications\/Marketing\/<\/code> so retried jobs carrying serialized <code>Neptune\\Domains\\CRM\\Models\\Lead<\/code> instances are accepted (matches Laravel <code>$notifiable<\/code> convention)<\/li>\n<li><strong>FIX<\/strong> <code>SendSalesSequenceEmailJob<\/code>: replace <code>$this-&gt;lead<\/code> with <code>$this-&gt;resolveLead()<\/code> to match the parent class refactor, fixing production ErrorException on undefined property<\/li>\n<li><strong>ADD<\/strong> Google Business Profile auto-publishing: daily scheduled command (<code>gbp:publish-post<\/code>) picks the oldest unposted published blog post and pushes it to GBP as a STANDARD\/LEARN_MORE post; <code>gbp:authorize<\/code> handles one-time OAuth setup via real callback route (<code>\/auth\/gbp\/callback<\/code>); <code>gbp_posted_at<\/code> column on posts tracks status; <code>GoogleBusinessProfileService<\/code> handles token refresh + API call; GBP badge in Filament PostResource; <code>docs\/integrations\/<\/code> folder with full GBP reference and third-party services index<\/li>\n<li><strong>ADD<\/strong> Blog posts FR+EN: &quot;Quel est le meilleur logiciel de gestion de flotte au Senegal ?&quot; and &quot;Est-ce legal de geolocaliser les vehicules de ses employes ?&quot;; cross-link all 39 FR blog posts with internal <code>\/post\/slug<\/code> links for SEO and navigation (100+ links added across 34 posts); append 2 new post pairs to <code>PostSeeder<\/code><\/li>\n<li><strong>FIX<\/strong> <code>SendAbandonedCartSequenceListener<\/code>: guard against null <code>$order-&gt;buyer<\/code> before passing to <code>Lead::firstOrCreateLeadFrom()<\/code>, preventing TypeError in production when an order has no associated buyer<\/li>\n<li><strong>ADD<\/strong> Blog post FR+EN: &quot;Comment fonctionne un traceur GPS sans abonnement ?&quot; (how GPS-GSM chain works, SMS mode, free app mode, SIM requirement, feature comparison table); update <code>cout-installation-gps-voiture-dakar<\/code> + EN equivalent with Premium-first pricing table, annual plan framing (100 000 FCFA\/an, 2 mois offerts), and cross-link to new post; append post pair to <code>PostSeeder<\/code><\/li>\n<li><strong>FIX<\/strong> <code>DispatchOutboundDealershipCommand<\/code>: exclude <code>noSub_<\/code>-prefixed emails from the company query so flagged-undeliverable addresses are never dispatched, protecting sender reputation<\/li>\n<li><strong>FIX<\/strong> <code>UserAgentParser::getDeviceType()<\/code>: add <code>okhttp<\/code> pattern for React Native Android and <code>cfnetwork<\/code>\/<code>darwin<\/code> for React Native iOS so mobile-app user-agents no longer fall through to &quot;Unknown&quot;; add <code>okhttp<\/code> to <code>isMobile()<\/code> accordingly<\/li>\n<li><strong>FIX<\/strong> <code>TrackApiCallMiddleware<\/code>: persist <code>user_agent<\/code> in <code>deviceData<\/code> for API visitors so the raw UA string is stored in MongoDB (enables future UA-based backfill)<\/li>\n<li><strong>ADD<\/strong> <code>visitors:backfill-device-type<\/code> command: backfill <code>device_type<\/code> (and <code>device_brand<\/code>, <code>device_os<\/code>, <code>browser<\/code>) for the 738 existing visitors with null device_type; re-parses stored <code>user_agent<\/code> when available, falls back to &quot;Mobile App&quot; for platform=api or browser=Traxelio App visitors, with <code>--dry-run<\/code> and <code>--chunk<\/code> options<\/li>\n<li><strong>MOD<\/strong> Jobs: move DB lookups out of constructors into <code>handle()<\/code> for <code>CreateTransactionFromPaymentJob<\/code>, <code>ProcessThirdPartyPaymentJob<\/code>, <code>MarkLeadAsLostJob<\/code>, and <code>CreateMissingEventJob<\/code> so jobs can be dispatched with a plain ID and queries only run at execution time<\/li>\n<li><strong>FIX<\/strong> <code>SendSequenceEmailJob<\/code>: stop serializing <code>$lead<\/code> in constructor; add <code>resolveLead()<\/code> to fetch fresh from DB at execution time, fixing TypeError when old serialized jobs (with <code>Neptune\\Domains\\CRM\\Models\\Lead<\/code>) run against the updated <code>toMail(App\\Models\\Lead)<\/code> signature; decouple 13 CRM package files from <code>App\\Models\\Lead<\/code> by replacing direct imports with config-based queries and untyped params with PHPDoc<\/li>\n<li><strong>FIX<\/strong> <code>SubscriptionHealthChart<\/code>: reduce <code>computeChurnRates()<\/code> from 62 per-day queries to 1 by fetching all subscriptions (with <code>cancelled_at<\/code>) upfront and computing churn in PHP collections; clean up inline FQCN type hints across all 5 compute methods (fixes TRAXELIO-6C)<\/li>\n<li><strong>FIX<\/strong> <code>SubscriptionHealthChart<\/code>: merge 2 DB queries per day into 1 in <code>computeCustomerConcentration()<\/code> by fetching per-customer totals via <code>GROUP BY<\/code> and deriving gross sales + top customer in PHP, halving queries from 62 to 31 (fixes TRAXELIO-6E)<\/li>\n<li><strong>MOD<\/strong> UserController: eager-load <code>subscriptions.plan<\/code>, <code>subscriptions.subscriber<\/code>, <code>subscriptions.product<\/code> on both <code>index<\/code> and <code>me<\/code> endpoints to prevent N+1 when serializing subscription relations<\/li>\n<li><strong>MOD<\/strong> Traccar: switch logging from console to file (<code>\/opt\/traccar\/logs\/tracker-server.log<\/code>) with daily rotation; add <code>traccar-disabled<\/code> fail2ban filter+jail to ban IPs sending disabled-device probes; update healthcheck to use correct log filename and filter <code>Geocoding failed 429<\/code> and <code>Set property error deserialize<\/code> as expected noise; fix Laravel log path and add queue-size monitoring (warn at 100, fail at 500); remove hardcoded IP range from <code>traccar-probe.local<\/code> ignoreip<\/li>\n<li><strong>FIX<\/strong> <code>CartIcon<\/code>: convert <code>$count<\/code> from a public typed property to a <code>#[Computed]<\/code> method to prevent <code>TypeError<\/code> when Livewire snapshot hydration assigns an array to a typed <code>int<\/code> property<\/li>\n<li><strong>MOD<\/strong> GeoIP lookup moved from <code>SessionController<\/code> loop to login time: new migration adds <code>city<\/code>\/<code>country<\/code> columns to <code>personal_access_tokens<\/code>; all four login paths (AuthController, GoogleLoginController, AppleLoginController, GuestLoginController) resolve and persist geo once per token; <code>GET \/api\/auth\/sessions<\/code> reads columns directly \u2014 no outbound HTTP on session list<\/li>\n<li><strong>FIX<\/strong> GeoIP\/UserAgentParser: replace <code>Cache::has() + Cache::get()<\/code> with single <code>Cache::get()<\/code> in GeoIP (eliminates 1 redundant DB query per cache hit), and add in-process static memoization to both <code>GeoIP::lookup()<\/code> and <code>UserAgentParser::parse()<\/code> so repeat calls within a request incur zero DB queries (fixes TRAXELIO-6A N+1 on store product listing)<\/li>\n<li><strong>FIX<\/strong> CheckoutReviewController: deduplicate Meta CAPI AddToCart and InitiateCheckout events per session using <code>checkout_fired.{productId}<\/code> session key, preventing duplicate job inserts and duplicate CRM activities on page refresh<\/li>\n<li><strong>ADD<\/strong> Meta CAPI: wire <code>country<\/code> (SHA256-hashed GeoIP ISO code) into all 9 conversion event types (Purchase, ViewContent, AddToCart, InitiateCheckout, CompleteRegistration, Lead, Schedule, Subscribe, ViewContent), propagated through 7 events, 2 lib marketplace controllers, 6 app controllers\/actions, 9 listeners, and <code>MetaPurchaseConversionJob<\/code>; <code>country<\/code> also persisted in <code>order-&gt;data<\/code> alongside <code>city<\/code> for deferred purchase events<\/li>\n<li><strong>MOD<\/strong> OrderResource: reorganize form into sections (Buyer, Summary, Payment, Status Timeline, Metadata), add status badge with semantic colors to table, replace raw ULID with <code>id_short<\/code> mono badge, add status\/paid\/unpaid filters, make AppointmentResource form sections responsive for mobile<\/li>\n<li><strong>ADD<\/strong> Checkout page abandoned funnel: <code>checkout:page-abandoned<\/code> command (hourly) detects visitors who viewed <code>product.checkout.show<\/code> or <code>store.product.checkout.review<\/code> 1-2h ago without completing a purchase, fires <code>CheckoutPageAbandonedEvent<\/code>, dispatches a 3-email sequence (immediate \u00b7 +2d \u00b7 +5d) for authenticated visitors not already in the abandoned cart funnel, auto-cancels batch on <code>PaymentCompletedEvent<\/code>, new <code>CHECKOUT_PAGE_ABANDONED<\/code> funnel entry in <code>FunnelEnum<\/code><\/li>\n<li><strong>ADD<\/strong> <code>--missing-geo<\/code> flag on <code>visitors:backfill-geo<\/code> to re-process visitors that have ISP but are missing region\/latitude\/longitude from the first backfill run<\/li>\n<li><strong>ADD<\/strong> Crawler UA filtering: block Lighthouse, Googlebot, AhrefsBot, and 17 other known crawlers in <code>TrackVisitorMiddleware<\/code> before any tracking; <code>CRAWLER_UA_PATTERNS<\/code> constant on <code>Visitor<\/code> model is single source of truth; <code>calculateBotProbability()<\/code> scores known crawler UAs at 80+ for backfill accuracy<\/li>\n<li><strong>ADD<\/strong> Multiple emails per user: <code>user_emails<\/code> table, <code>UserEmail<\/code> model, <code>UserEmailController<\/code> (store\/verify\/resend\/destroy), <code>VerifyAdditionalEmailNotification<\/code>, Emails card on profile page with primary badge, verified\/unverified status, Alpine.js add-email form, and extended <code>login<\/code> scope to authenticate via additional verified emails<\/li>\n<li><strong>ADD<\/strong> Active Devices API: <code>GET \/api\/user\/sessions<\/code> endpoint returns web sessions (from <code>sessions<\/code> table) and Sanctum API tokens with browser\/OS detection, GeoIP location, and <code>is_current<\/code> flag for the calling token<\/li>\n<li><strong>FIX<\/strong> CartIcon: bust stale cache key (<code>cart_item_count<\/code>) and add <code>(int)<\/code> cast to prevent TypeError when cache held a legacy array value<\/li>\n<li><strong>MOD<\/strong> DeviceController index: eager-load <code>position<\/code> by default, remove <code>subscription<\/code>\/<code>subscription.plan<\/code>\/<code>subscription.product<\/code> from opt-in relations (heavy data belongs on device.show only); remove <code>with=position<\/code> from mobile app <code>fetchUserDevices<\/code> call<\/li>\n<li><strong>ADD<\/strong> Bot probability scoring: additive bot_probability (0-100) on Visitor model with datacenter detection, unknown UA\/browser, zero engagement, missing language\/screen signals; persisted on every activity; Filament column (badge, color-coded), range filter, and session duration filter on VisitorResource and VisitsRelationManager<\/li>\n<li><strong>ADD<\/strong> <code>visitors:backfill-geo<\/code> command: backfill ISP\/org\/ASN via GeoIP lookup for existing visitors and recompute bot_probability, with throttling for ip-api.com rate limits<\/li>\n<li><strong>MOD<\/strong> GeoIP: use ip-api.com X-Rl\/X-Ttl response headers for self-throttling instead of arbitrary delay; remove <code>--delay<\/code> option from backfill command<\/li>\n<li><strong>FIX<\/strong> HasViews::recordView(): pass full deviceData (UA, GeoIP, Context fields) to RecordVisitorActivityJob so visitors arriving via excluded routes (store.product.landing, store.product.show) get device\/browser\/ISP data populated on first visit<\/li>\n<li><strong>MOD<\/strong> Move GeoIP and UserAgentParser from <code>app\/Support\/<\/code> to <code>lib\/crm\/src\/Support\/<\/code> (namespace <code>Neptune\\Domains\\CRM\\Support<\/code>); update all imports across app controllers, middlewares, services, and Blade views<\/li>\n<li><strong>FIX<\/strong> AppServiceProvider: use non-enforcing <code>Relation::morphMap()<\/code> instead of <code>enforceMorphMap()<\/code> so old queued jobs serialized with <code>Neptune\\Domains\\CRM\\Models\\Lead<\/code> do not crash with ClassMorphViolationException<\/li>\n<li><strong>MOD<\/strong> Code style: apply php-cs-fixer formatting to Job middlewares, User model, DeviceActivationNotification, SellerSeeder, and product landing view<\/li>\n<li><strong>FIX<\/strong> CrmAnalyticsService: rewrite all heavy methods to use MongoDB aggregation pipelines instead of loading full documents into PHP memory; fixes OOM crash on dashboard\/visitors pages with 33k+ activity records<\/li>\n<li><strong>FIX<\/strong> UserPreferenceMiddleware: always resolve GeoIP country for visitor tracking, independent of currency detection; previously authenticated users with saved currency preferences had null country_code stored<\/li>\n<li><strong>FIX<\/strong> Header: fix cart icon vertical alignment on desktop (flex items-center)<\/li>\n<li><strong>FIX<\/strong> OrdersRevenueChart: filter by <code>paid_at<\/code> instead of <code>created_at<\/code> (only count paid orders), remove erroneous <code>\/100<\/code> division (prices stored as whole XOF, not subunits), display integers for XOF amounts<\/li>\n<li><strong>ADD<\/strong> DashboardTest: add <code>testAdminCanAccess<\/code> positive test case<\/li>\n<li><strong>MOD<\/strong> Checkout success: remove extra nav buttons, pre-fill store\/product on &quot;Activate GPS&quot; CTA so device.new auto-skips store and product selection steps<\/li>\n<li><strong>FIX<\/strong> CTA button hover: replace dark gradient shift with brightness filter to keep white text visible<\/li>\n<li><strong>MOD<\/strong> Footer: remove Twitter\/X social link<\/li>\n<li><strong>MOD<\/strong> Carousel branded slides: use light background on all 5 slides (was only slides 3-5), update text colors on hook\/problem slides for light bg<\/li>\n<li><strong>MOD<\/strong> Carousel: add Intent dimension to folder structure and composition IDs (PC-Shop-Pas-...), all 20 carousels tagged as &quot;shop&quot; intent<\/li>\n<li><strong>FIX<\/strong> CrmAnalyticsService: rewrite all queries to use MongoDB Eloquent models (Visitor, Visit, VisitorActivity) instead of MySQL DB::table() which referenced tables that no longer exist on prod\\n- <strong>FIX<\/strong> Complete CARD\u2192STRIPE rename: update remaining controllers (Cart, DeviceNew, Order, Invoice, Product), marketplace checkout controllers, cashier lib enum, ProcessStripeEventJobTest, add payments table migration to backfill <code>method<\/code> column, add payment-stripe.svg icon<\/li>\n<li><strong>FIX<\/strong> Remove BlockedVisitor feature: delete BlockedVisitor model and BlockedVisitorTypeEnum from CRM lib, remove isBlocked() middleware check, remove block IP\/UA Filament actions from VisitorResource pages, delete blocked-visitors blade modal<\/li>\n<li><strong>FIX<\/strong> Remove Metrics system: delete Metric model, MetricTypeEnum, MilestoneReachedEvent, ComputeMetricsCommand, metrics table migration, and remove DailyAnalyticsCaptureCommand and ComputeMetricsCommand from scheduler<\/li>\n<li><strong>MOD<\/strong> Carousel branded CTA slide: light background with orange circle accent behind product image on slides 3-5; update SlideLayout, Background, and ProductCarouselSlide to support BackgroundVariant prop<\/li>\n<li><strong>MOD<\/strong> Ad framework: add References section linking to ugly-ads-guide.md<\/li>\n<li><strong>FIX<\/strong> OrderItem product link: populate <code>product_type<\/code>\/<code>product_id<\/code> on subscription and installation items in <code>ValidatesOrderForCheckout<\/code>, and on renewal items in <code>SubscriptionRenewController<\/code>; bind <code>neptune-cashier.models.subscription-plan<\/code> to <code>App\\Models\\SubscriptionPlan<\/code> so morph map is respected (closes #377)<\/li>\n<li><strong>FIX<\/strong> Code review: remove stale CARD comment in DevicePaymentController, wrap Coupon::generateForUser in DB::transaction with lockForUpdate to prevent race-condition double-insert, update Stripe fixture expires_at to far-future timestamp so ProcessStripeEventJobTest no longer throws PaymentEventExpiredException<\/li>\n<li><strong>MOD<\/strong> PaymentMethodEnum: rename <code>CARD = 'card'<\/code> to <code>STRIPE = 'stripe'<\/code> across all PHP enums, controllers, actions, tests, migration, SVG icon, and mobile-app enum; add DB migration to backfill existing <code>payments.method<\/code> rows from <code>card<\/code> to <code>stripe<\/code><\/li>\n<li><strong>ADD<\/strong> Healthcheck: persistent metrics log (cpu\/mem\/disk\/swap\/response_ms per run), min\/max\/avg stats over 24h\/week\/month (checks 21-24), all-time record tracking with alert on new record, response time captured via curl time_total (closes #264)<\/li>\n<li><strong>MOD<\/strong> Ad framework: add Intent as Dimension 0 (Shop \u2192 store.product.landing, Track \u2192 device.new), update recipe format, composition IDs, folder structure, and render commands; add Bundle angle carousels (4 ICPs x bundle = 20 total)<\/li>\n<li><strong>FIX<\/strong> Meta tracking: remove client-side ViewContent pixel call (was double-counting with server-side CAPI), capture <code>utm_id<\/code> through full pipeline (cookie, Visitor model, order.data across all 4 checkout controllers)<\/li>\n<li><strong>ADD<\/strong> Healthcheck: Traccar error log scanner (check 18b, skips geocoding noise) and socket listener checks via supervisorctl + Laravel log (checks 19b\/19c), bringing total to 23 checks (closes #391, #392)<\/li>\n<li><strong>ADD<\/strong> Server healthcheck script (<code>devops\/scripts\/healthcheck.sh<\/code>) with 20 checks (services, resources, application), state-based alerting (no duplicates), and <code>make healthcheck<\/code> target for remote dry-run<\/li>\n<li><strong>ADD<\/strong> <code>SkipIfAlreadySent<\/code> job middleware to prevent duplicate email sends on retry, restore <code>$tries = 3<\/code> on <code>SendSequenceEmailJob<\/code><\/li>\n<li><strong>FIX<\/strong> CRM morph map violation: resolve Lead via config in <code>SendSequenceEmailJob<\/code> and <code>MarkLeadAsLostJob<\/code> to use <code>App\\Models\\Lead<\/code> (registered in morph map) instead of base CRM Lead<\/li>\n<li><strong>MOD<\/strong> ActivityResource: add lead and notification class filters, show entity name instead of type, add toggleable metadata column<\/li>\n<li><strong>MOD<\/strong> Outbound dealership email copy: rewrite all 5 notifications for shorter, punchier messaging<\/li>\n<li><strong>ADD<\/strong> Product carousel ad compositions (ICP x Angle x ContentType), OG image composition, screenshot paths on features, ad frameworks, and tweets content<\/li>\n<li><strong>FIX<\/strong> Store product landing: deduplicate feature list by excluding the top 3 features (shown above CTA) from the full collapsible list below<\/li>\n<li><strong>MOD<\/strong> Non-XOF store checkout: show structured address form with auto-resolved shipping zone instead of zone picker buttons, require address fields for non-XOF stores even when zone is resolved<\/li>\n<li><strong>FIX<\/strong> VisitorScoreChangedEvent TypeError: allow null previousScore for first-time visitor score calculations<\/li>\n<li><strong>FIX<\/strong> Lead ClassMorphViolationException: point morph map and CRM config to App\\Models\\Lead (child class) instead of base CRM Lead<\/li>\n<li><strong>FIX<\/strong> Product page: fix plan\/duration badge text overlap on mobile (flex-wrap on badge rows), remove thumbnail click-to-swap-featured-image behavior, add collapsible description on landing page<\/li>\n<li><strong>MOD<\/strong> Checkout: hide email\/phone fields for logged-in users (pass as hidden inputs), show identity card with &quot;Not you?&quot; logout link that redirects back to checkout, add session invalidation on logout<\/li>\n<li><strong>FIX<\/strong> Product image resize job: switch from Imagick to GD driver (prod has GD only), use <code>ImageManager::gd()<\/code> for easy driver swap<\/li>\n<li><strong>FIX<\/strong> Filament Select null label crash on LeadResource ListLeads: filter empty\/null entries from source and campaign SelectFilter options (MongoDB pluck can return null labels)<\/li>\n<li><strong>FIX<\/strong> Order notification race conditions: gate upsell listener on <code>paid_at<\/code> with queue release retry, move low stock check into <code>DecrementStockListener<\/code> after decrement, add 24h cache guard to prevent duplicate low stock alerts<\/li>\n<li><strong>ADD<\/strong> Order fulfillment notifications: notify store owner on payment received (<code>OrderPaidNotification<\/code> via mail + WhatsApp), CC store owner on abandoned carts (<code>CartAbandonedSellerNotification<\/code>), post-purchase subscription upsell with 3-day delay (<code>SubscriptionUpsellNotification<\/code>), low stock alert when product stock drops to 5 or below (<code>LowStockNotification<\/code> via mail + WhatsApp)<\/li>\n<li><strong>FIX<\/strong> Header mobile horizontal scroll: add <code>overflow-x-hidden<\/code> to body layouts, hide cart and profile on mobile, reduce CTA button padding on small screens<\/li>\n<li><strong>MOD<\/strong> Reorganize footer into 5 columns (Product, Shop, Resources, Company) with Cart and My Account links in new Shop section<\/li>\n<li><strong>MOD<\/strong> Update subscription plan prices for US and Canada markets: USD Premium $20\u2192$29, Platinum $250\u2192$299; CAD Basic $12\u2192$13, Premium $25\u2192$39, Platinum $325\u2192$399; update dealership nudge email to reflect new Basic price<\/li>\n<li><strong>MOD<\/strong> Extract outbound sequence dispatch logic into <code>DispatchOutboundSequenceAction<\/code>, add outbound trigger button and bulk action to LeadResource, rewrite dealership email copy for higher engagement<\/li>\n<li><strong>FIX<\/strong> Bundle checkout: apply installation cost to total GPS units (quantity + bonus) in controller, review, and view<\/li>\n<li><strong>FIX<\/strong> Missing <code>ProductFactory<\/code> import in Product model, route param <code>{storeProduct}<\/code> renamed to <code>{product}<\/code>, dead <code>storeProducts()<\/code> alias removed from Store, <code>$this-&gt;storeProduct<\/code> bug fixed in <code>NewStoreOrderNotification<\/code><\/li>\n<li><strong>ADD<\/strong> <code>&lt;x-store.shop-cta \/&gt;<\/code> component, added to features, industries, trackers, and brand landing pages<\/li>\n<li><strong>MOD<\/strong> MongoDB <code>analytics_events<\/code> index: replace <code>created_date<\/code> compound index with <code>entity_id + machine_name<\/code> for better query performance<\/li>\n<li><strong>ADD<\/strong> Self-hosted email tracking for outbound sales: open pixel (1x1 GIF), click-through link wrapping, bounce detection via TransportException, all recorded as CRM activities with tracking metadata (tracking_id, funnel, stage, notification_class)<\/li>\n<li><strong>FIX<\/strong> CASL\/CAN-SPAM compliance: extend unsubscribe link expiry from 30 to 60 days, add physical mailing address to outbound email signature<\/li>\n<li><strong>ADD<\/strong> Per-product bundle tiers (&quot;buy X, get Y free&quot;): <code>product_bundles<\/code> table, <code>ProductBundle<\/code> model, <code>resolveBundle()<\/code> on Product, Filament repeater admin, landing page Alpine.js tier cards with bonus message, checkout review bonus GPS line, checkout controller bonus order item (price=0) with stock validation (<code>quantity + bonus &lt;= stock<\/code>)<\/li>\n<li><strong>MOD<\/strong> Rename <code>StoreProduct<\/code> model to <code>Product<\/code> in marketplace package, rename legacy <code>App\\Models\\Product<\/code> to <code>LegacyProduct<\/code>, rename DB tables (<code>products<\/code> -&gt; <code>legacy_products<\/code>, <code>store_products<\/code> -&gt; <code>products<\/code>), rename <code>$storeProduct<\/code> variables to <code>$product<\/code> across all controllers\/views\/tests\/listeners, rename event properties (<code>storeProductId<\/code> -&gt; <code>productId<\/code>, etc.)<\/li>\n<li><strong>FIX<\/strong> <code>MigrateAnalyticsToMongoCommand<\/code>: add idempotency guard (errors if collection non-empty without <code>--fresh<\/code>) and confirmation prompt before destructive <code>--fresh<\/code> drop<\/li>\n<li><strong>FIX<\/strong> <code>MergeUsersCommand<\/code>: patch both top-level <code>user_id<\/code> (new MongoDB docs) and nested <code>data.user_id<\/code> (historical MySQL-migrated docs) when merging users<\/li>\n<li><strong>FIX<\/strong> <code>config\/neptune-analytics.php<\/code>: change default connection from <code>mongodb<\/code> to <code>mysql<\/code> to avoid split-brain state before migration runs on prod<\/li>\n<li><strong>ADD<\/strong> <code>ANALYTICS_CONNECTION=mysql<\/code> to <code>.env.example<\/code><\/li>\n<li><strong>MOD<\/strong> <code>.env.production<\/code>: set <code>ANALYTICS_CONNECTION=mongodb<\/code> now that migration is complete and MySQL table dropped<\/li>\n<li><strong>MOD<\/strong> LeadResource: add nav badge (unverified last 7d), stats overview widget, bucket\/company\/city\/industry columns, bucket\/verified\/phone\/source\/campaign\/industry filters, &quot;Add to Funnel&quot; and &quot;Recalculate Scores&quot; bulk actions, sectioned form with metadata fields, and read-only ViewLead infolist page<\/li>\n<li><strong>ADD<\/strong> Outbound dealership email sequence: 5-email cold outbound flow (Intro, ValueDrop, Story, Nudge, Breakup) sent from founder email via <code>founder_smtp<\/code> mailer, targeting Toronto car dealership companies in CRM<\/li>\n<li><strong>ADD<\/strong> <code>OUTBOUND_DEALERSHIP<\/code> funnel in <code>FunnelEnum<\/code> with <code>new<\/code>\/<code>lost<\/code> stages<\/li>\n<li><strong>ADD<\/strong> <code>outbound:dealership<\/code> artisan command to dispatch the sequence with <code>--dry-run<\/code> preview and <code>--city<\/code> filter<\/li>\n<li><strong>MOD<\/strong> UTM cookies: switch from first-touch to last-touch attribution so returning visitors from a new campaign get credit for the latest campaign<\/li>\n<li><strong>ADD<\/strong> Store UTM params (source, medium, campaign, content, term) in <code>order.data<\/code> across all checkout flows (Product, Marketplace, DeviceNew, Order Pay)<\/li>\n<li><strong>ADD<\/strong> <code>visitor_fingerprint<\/code> to Marketplace checkout <code>order.data<\/code> (was previously missing)<\/li>\n<li><strong>ADD<\/strong> <code>utm_content<\/code> and <code>utm_term<\/code> columns on <code>visitors<\/code> table; persist them via TrackVisitorMiddleware and RecordVisitorActivityJob<\/li>\n<li><strong>ADD<\/strong> 13 popular GPS tracker brands (37 devices) to device catalog: Tracki, LandAirSea, Bouncie, Spytec, SpaceHawk, TKStar, WanWay, Jointech, Orbcomm, Samsara, Verizon Connect, Motive, Azuga; updated import command with brand domains for logo downloads<\/li>\n<li><strong>ADD<\/strong> <code>ProcessStoreProductMediaJob<\/code>: Imagick\/Intervention image resizing pipeline generating 3 WebP sizes (thumb 96x96, card 400x300, hero 800x600) stored under <code>conversions\/<\/code> on the <code>stores<\/code> disk; <code>StoreProduct<\/code> auto-dispatches the job when <code>featured_media<\/code> or <code>medias<\/code> change and exposes <code>featured_card_url<\/code> and <code>thumbUrl<\/code> on media items<\/li>\n<li><strong>ADD<\/strong> <code>2026_02_24_000000_add_medias_converted_at_to_store_products<\/code>: migration adding <code>medias_converted_at<\/code> timestamp to track when image conversions are ready; model falls back to original URL until conversions exist<\/li>\n<li><strong>MOD<\/strong> <code>store\/product\/index<\/code>, <code>show<\/code>, <code>landing<\/code>: use <code>featured_card_url<\/code> for listing cards; add <code>srcset<\/code>\/<code>sizes<\/code> on hero images; serve <code>thumbUrl<\/code> for gallery thumbnails with <code>data-full<\/code> attribute so the gallery JS loads the full-resolution URL on click<\/li>\n<li><strong>MOD<\/strong> <code>show.blade.php<\/code>: simplify installation toggle to a plain inline toggle row; replace WhatsApp\/phone big buttons with a compact &quot;Have a question?&quot; text link; add Tailwind prose overrides to constrain heading sizes in product descriptions<\/li>\n<li><strong>FIX<\/strong> <code>GoogleLoginController<\/code>: add <code>\/store\/<\/code> redirect handling so Google OAuth after checkout redirects back to the store page (same fix already applied to Apple)<\/li>\n<li><strong>FIX<\/strong> <code>create_geofences_table<\/code> migration: remove <code>traccar.run_migration<\/code> guard so the <code>tc_geofences<\/code> table is always created<\/li>\n<li><strong>FIX<\/strong> <code>create_orders_table<\/code> migration: remove orphan <code>Schema::table<\/code> block that added <code>shipping_cost<\/code>\/<code>shipping_zone<\/code> (already applied to production via separate migration)<\/li>\n<li><strong>FIX<\/strong> <code>AppleLoginController<\/code>: move Sanctum token creation to after all early-return redirects so no orphaned <code>personal_access_tokens<\/code> row is created when the OAuth flow ends in a <code>lead.<\/code>, <code>reseller.<\/code>, or <code>\/store\/<\/code> redirect<\/li>\n<li><strong>FIX<\/strong> <code>checkout.blade.php<\/code>: replace <code>substr()<\/code> with <code>mb_substr()<\/code> for avatar initial so multi-byte names (e.g. &quot;\u00c9lise&quot;) render the correct first character<\/li>\n<li><strong>ADD<\/strong> <code>lang\/fr.json<\/code>: 4 missing translations for social login checkout UI (&quot;Sign in for faster checkout&quot;, &quot;Continue with Google&quot;, &quot;Continue with Apple&quot;, &quot;or continue with email&quot;)<\/li>\n<li><strong>ADD<\/strong> <code>resources\/trackers\/<\/code>: local copies of all 8 featured tracker device images; <code>FeaturedTrackerDeviceSeeder<\/code> now reads from <code>resource_path('trackers\/...')<\/code> and uploads to <code>Storage::disk('trackers')<\/code> without any HTTP requests at deploy time<\/li>\n<li><strong>FIX<\/strong> <code>SellerSeeder<\/code>: revert inline markdown description image refs back to <code>.gif<\/code> (browsers cannot display MP4 via <code>&lt;img&gt;<\/code>); <code>images[]<\/code> array entries that go to S3 still use <code>.mp4<\/code><\/li>\n<li><strong>FIX<\/strong> <code>TrackVisitorMiddleware<\/code>: share newly generated fingerprint via <code>Context::add('visitor_fingerprint', ...)<\/code> so <code>CheckoutReviewController<\/code> can read it on first-visit requests where the cookie is in the response but not yet in the request<\/li>\n<li><strong>FIX<\/strong> <code>CheckoutReviewController<\/code>: fall back to <code>Context::get('visitor_fingerprint')<\/code> when <code>_trx_vid<\/code> cookie is absent (first-visit sessions) so <code>StoreProductCheckoutStartedEvent<\/code> always carries the fingerprint<\/li>\n<li><strong>FIX<\/strong> <code>AuthServiceProvider<\/code>: <code>Gate::define('device.subscription', ...)<\/code> now passes <code>[DeviceSubscriptionPolicy::class, '__invoke']<\/code> so Laravel resolves the invokable policy correctly<\/li>\n<li><strong>MOD<\/strong> <code>Device::scopeOffline()<\/code>: replace 4 correlated subqueries (2x <code>orWhereHas<\/code> + 2x <code>withCount<\/code>) with a single <code>LEFT JOIN<\/code> on a derived aggregation table; returns correct <code>on_off_events_count<\/code> and <code>low_battery_events_count<\/code> columns without N+1 queries<\/li>\n<li><strong>MOD<\/strong> Facebook Pixel (<code>pixel.blade.php<\/code>): defer initialisation to after the <code>load<\/code> event + <code>requestIdleCallback<\/code> so <code>fbevents.js<\/code> no longer competes with critical resources during page startup<\/li>\n<li><strong>MOD<\/strong> Installation toggle in <code>show.blade.php<\/code>: redesigned as a full-width card button with gradient icon, &quot;Recommended&quot; badge, active glow ring, and technician hint line<\/li>\n<li><strong>ADD<\/strong> <code>public\/images\/features\/smart_tracking.mp4<\/code>: ffmpeg-converted MP4 (224KB vs 1.3MB GIF, 83% smaller); <code>SellerSeeder<\/code> switched to MP4 for all product media arrays and inline description markdown refs<\/li>\n<li><strong>ADD<\/strong> <code>resources\/products\/<\/code>: local copies of all 26 product images (coban-gps303, concox-gt06n, c32, cj720, bouncie-gps-tracker, landairsea-54, tracki-pro, planetgps-obd-4g); <code>SellerSeeder<\/code> updated to use <code>resource_path()<\/code> instead of remote URLs so seeding never depends on third-party image hosts<\/li>\n<li><strong>FIX<\/strong> <code>Device::scopeIgnition()<\/code> and <code>Position::scopeIgnition()<\/code>: query <code>ignition<\/code> column directly instead of <code>attributes-&gt;ignition<\/code> (column was promoted out of JSON)<\/li>\n<li><strong>MOD<\/strong> Device API: remove automatic eager-loading of <code>position<\/code> relation from <code>DeviceController::index()<\/code> and <code>User\\DeviceController::index()<\/code>; position is now opt-in via <code>?with=position<\/code><\/li>\n<li><strong>MOD<\/strong> <code>PositionUpdateEvent<\/code>: set <code>$tries = 1<\/code> to cap Reverb broadcast retries for high-frequency position updates<\/li>\n<li><strong>MOD<\/strong> Makefile: remove <code>supervisorctl restart all<\/code> from <code>deploy-app<\/code> (redundant after <code>queue:restart<\/code>); make <code>health<\/code> and <code>rotate-logs<\/code> pass <code>PROD_HOST<\/code>\/<code>PROD_KEY<\/code> env vars to deploy.sh<\/li>\n<li><strong>MOD<\/strong> <code>devops\/deploy.sh<\/code>: <code>PROD_HOST<\/code>, <code>PROD_USER<\/code>, <code>PROD_KEY<\/code> are now env-var overrideable with defaults preserved<\/li>\n<li><strong>MOD<\/strong> <code>devops\/cloudformation\/rds.yml<\/code>: set <code>innodb_buffer_pool_size<\/code> to 3\/4 of DBInstanceClassMemory and <code>innodb_buffer_pool_instances = 4<\/code> in RDS parameter group<\/li>\n<li><strong>FIX<\/strong> Remove already-applied <code>2026_02_23_000001_add_is_bot_to_visitors_table<\/code> migration file (was already run on production)<\/li>\n<li><strong>ADD<\/strong> Store product media: <code>ProductMediaItem<\/code> DTO, <code>mediaItems()<\/code> attribute on <code>StoreProduct<\/code>, <code>ConvertProductGifToVideoJob<\/code> (ffmpeg GIF-to-MP4), and <code>store:convert-gifs<\/code> artisan command; landing and show pages render <code>&lt;video autoplay loop muted playsinline&gt;<\/code> when media is a video file, with explicit width\/height on thumbnails for CLS; Tawk chat widget suppressed on store funnel pages<\/li>\n<li><strong>FIX<\/strong> <code>UserController::profile()<\/code>: only redirect to <code>traxelio:\/\/home?token=<\/code> when a <code>token<\/code> query param is actually present; previously any mobile user-agent hitting <code>\/user\/profile<\/code> without a token was redirected to the app with an empty token string<\/li>\n<li><strong>FIX<\/strong> CRM: exclude <code>store.product.show<\/code>, <code>store.product.landing<\/code>, and <code>store.product.checkout.review<\/code> from <code>TrackVisitorMiddleware<\/code> so each visit is counted once: product pages record <code>product_viewed<\/code> via <code>HasViews::recordView()<\/code>, checkout review records <code>checkout_started<\/code> on form submit via <code>RecordCheckoutStartedActivityListener<\/code><\/li>\n<li><strong>MOD<\/strong> Supervisor: standardise all process log paths to <code>\/var\/log\/traxelio\/<\/code> (was <code>\/var\/log\/supervisor\/<\/code> for laravel-worker, emails, redis-default, updates, scheduler, socket-listen)<\/li>\n<li><strong>ADD<\/strong> Supervisor <code>queue-redis-default<\/code>: new worker process consuming the Redis <code>default<\/code> queue so abandoned cart email batches (dispatched without an explicit queue name) are actually processed<\/li>\n<li><strong>FIX<\/strong> Cashier orders migration: add <code>shipping_cost<\/code> (nullable) and <code>shipping_zone<\/code> columns to existing table before the <code>Schema::create<\/code> block so the migration applies cleanly to production<\/li>\n<li><strong>ADD<\/strong> Creativa: <code>Coban303PulseAd<\/code> (15s \u00b7 450f) \u2014 PULSE concept: GPS rings expand from dark canvas, pain-hook question (&quot;Votre camion bouge la nuit ?&quot;), device drops with bounce, &quot;COBAN&quot; character-by-character stagger, benefit chips, live tracking GIF, CTA; registered as <code>Coban303Pulse-Vertical<\/code><\/li>\n<li><strong>ADD<\/strong> Creativa: <code>Coban303FantomeAd<\/code> (15s \u00b7 450f) \u2014 FANT\u00d4ME concept: truck SVG draws itself stroke-by-stroke (<code>strokeDashoffset<\/code> animation), ghosts via CSS blur+opacity ramp, red &quot;POSITION INCONNUE&quot; badge slams in, 1-frame yellow hard cut, device drops, truck re-solidifies, stroke flashes yellow at GPS connection moment, benefit chips, tagline &quot;Rendez l'invisible visible.&quot;; registered as <code>Coban303Fantome-Vertical<\/code><\/li>\n<li><strong>ADD<\/strong> <code>VisitResource::infolist()<\/code>: Session section (visitor link, platform, IP, started\/ended\/duration) and Navigation section (entry\/exit page, pages count, referrer); previously <code>ViewVisit<\/code> page was empty<\/li>\n<li><strong>ADD<\/strong> <code>is_bot<\/code> column to <code>visitors<\/code> MySQL table: migration backfills bots via UA pattern match + null-UA\/no-user_id\/no-page_view heuristic; <code>CrmAnalyticsService<\/code> adds <code>is_bot = 0<\/code> filter (JOIN on visitor_activities\/visits queries, direct where on visitors queries) so all analytics widgets exclude bots<\/li>\n<li><strong>MOD<\/strong> Store landing page CRO: 10 conversion-rate improvements: inline checkout (form POSTs to <code>store.product.checkout<\/code> without navigating away), countdown timer (offer ends 2026-03-16), social proof strip, urgency badge, top-3 features above CTA, LCP image <code>fetchpriority=&quot;high&quot;<\/code>, thin social data guard (views &gt;= 50), price anchor (&quot;Includes Premium plan&quot;), WhatsApp\/Call demoted to text links, <code>StoreProductAddedToCartEvent<\/code> fired on landing; <code>CheckoutController<\/code> now fires <code>StoreProductCheckoutStartedEvent<\/code> before payment redirect; <code>GeoIP<\/code> and <code>$promotionEndsAt<\/code> passed to landing view<\/li>\n<li><strong>FIX<\/strong> <code>CrmAnalyticsService::visitorsOverTime()<\/code>: filter <code>pageviewsPerDay<\/code> to <code>activity_type = page_view<\/code>; previously counted all activity types so bot API calls (64k) inflated the Pageviews dataset, compressing the Unique Visitors line to appear flat on a shared Y axis<\/li>\n<li><strong>FIX<\/strong> <code>CrmAnalyticsService<\/code>: rewrite all 9 methods from MongoDB aggregation pipelines to MySQL queries using <code>visitor_activities<\/code>, <code>visitors<\/code>, and <code>visits<\/code> tables; fix <code>pluck(&quot;_id&quot;)<\/code> to <code>pluck(&quot;id&quot;)<\/code> in all 6 visitor analytics widgets; wrap <code>HeatmapWidget<\/code> in try\/catch so missing MongoDB connection no longer silently crashes all dashboard widgets<\/li>\n<li><strong>FIX<\/strong> <code>VisitResource<\/code> visitor filter: replace <code>-&gt;relationship()-&gt;preload()<\/code> with <code>getSearchResultsUsing<\/code>\/<code>getOptionLabelUsing<\/code> to handle visitors with null names (Filament's <code>isOptionDisabled<\/code> requires a string label, crashes on null)<\/li>\n<li><strong>MOD<\/strong> Store checkout: autofill city and country in delivery address from GeoIP context (<code>CheckoutReviewController<\/code> passes <code>$geoip<\/code>, <code>checkout.blade.php<\/code> forwards <code>:geoip<\/code> to <code>x-input.address-form<\/code>)<\/li>\n<li><strong>MOD<\/strong> <code>GeoIP::lookup()<\/code>: <code>country<\/code> key now returns the ISO code (e.g. &quot;SN&quot;) instead of full name; drop <code>country_code<\/code> alias; update all callers (<code>UserPreferenceMiddleware<\/code>, <code>PlacesService<\/code>, <code>DeviceNewController<\/code>, <code>address-form.blade.php<\/code>); use <code>CountryEnum::getLabel()<\/code> for the <code>user_location<\/code> display string<\/li>\n<li><strong>MOD<\/strong> <code>GeoIP::lookup()<\/code>: add <code>ipapi.co<\/code> as fallback when ip-api.com fails or times out; extract two protected methods <code>lookupViaIpApi<\/code> \/ <code>lookupViaIpApiCo<\/code>; swallow <code>ConnectionException<\/code> silently on both providers; only cache successful results<\/li>\n<li><strong>FIX<\/strong> Makefile deploy: split <code>receive<\/code> into git-pull-only + <code>$(MAKE) deploy-app<\/code> re-invocation so the freshly-pulled Makefile is read by the new make process (previously <code>php artisan optimize<\/code> from the pre-pull Makefile would still run and fail on Filament view compilation); replace <code>php artisan optimize<\/code> with individual <code>config:cache<\/code> + <code>event:cache<\/code> + <code>route:cache<\/code> (skip <code>view:cache<\/code> which fails on <code>&lt;x-filament::widgets&gt;<\/code>)<\/li>\n<li><strong>FIX<\/strong> Makefile <code>prod<\/code>\/<code>node_modules<\/code> targets: derive <code>NODE_BIN<\/code> from npm path and prepend to <code>PATH<\/code> so <code>node<\/code> is found during non-interactive SSH deploys (NVM bin dir not in PATH otherwise)<\/li>\n<li><strong>MOD<\/strong> Makefile: extract SSH connection info (<code>SSH_HOST<\/code>, <code>SSH_USER<\/code>, <code>SSH_KEY<\/code>, <code>SSH<\/code>) as variables at the top; all deploy, ssh, rollback, releases targets now use <code>$(SSH)<\/code> macro<\/li>\n<li><strong>MOD<\/strong> Store product show page: fix wide-screen layout (replace <code>container max-w-5xl<\/code> with <code>w-full max-w-5xl<\/code> to prevent <code>container<\/code> media query from overriding <code>max-w-5xl<\/code>); add <code>product-description<\/code> CSS class to description div; add <code>loading=&quot;lazy&quot;<\/code> to markdown-rendered images<\/li>\n<li><strong>MOD<\/strong> Store product listing: strip markdown from description preview using <code>strip_tags(Str::markdown($desc))<\/code> so raw <code>##<\/code> and <code>![]()<\/code> symbols no longer show in the card<\/li>\n<li><strong>MOD<\/strong> <code>SellerSeeder<\/code>: unify all 4 Dakar XOF products under a single French <code>XOF_DESCRIPTION<\/code> constant; move 3 feature images (smart tracking GIF, mileage report, immobilizer) from markdown body into each product's <code>images<\/code> array<\/li>\n<li><strong>ADD<\/strong> <code>app.scss<\/code>: <code>.product-description img<\/code> rule with <code>max-height: 24rem<\/code>, <code>width: auto<\/code>, <code>border-radius: 0.75rem<\/code>, and <code>object-fit: contain<\/code><\/li>\n<li><strong>FIX<\/strong> <code>Post::views()<\/code> recurring crash: remove <code>-&gt;withCount('views')<\/code> from <code>Livewire\/Post\/Directory<\/code>, remove <code>views_count<\/code> column and <code>ViewsRelationManager<\/code> from <code>PostResource<\/code> (relationship no longer exists after <code>HasViews<\/code> was refactored to use <code>VisitorActivity<\/code> directly)<\/li>\n<li><strong>MOD<\/strong> Store checkout: use product currency consistently across <code>CheckoutReviewController<\/code> and <code>CheckoutController<\/code>; normalize XOF vs cents-based prices so Stripe line items are correct; pass formatted <code>storeProductPriceText<\/code> and <code>installationPriceText<\/code> to <code>checkout.blade.php<\/code>; add CAD pricing tier to <code>PlanEnum<\/code>; <code>ProductController<\/code> now uses product currency for CAPI events<\/li>\n<li><strong>MOD<\/strong> <code>plan-picker<\/code> component: accept optional <code>prices<\/code> prop to override displayed price text per plan; fix sort to use <code>getOrder()<\/code> instead of <code>getPrice()<\/code>; <code>store\/product\/show<\/code> passes <code>$planPrices<\/code> from controller<\/li>\n<li><strong>MOD<\/strong> <code>store\/product\/landing<\/code> page: convert from standalone HTML to <code>@extends('layout')<\/code> with overridable <code>@section('header')<\/code> and <code>@section('footer')<\/code>; <code>layout.blade.php<\/code> now supports <code>@hasSection<\/code> for header\/footer override<\/li>\n<li><strong>MOD<\/strong> <code>features\/show.blade.php<\/code>: remove fixed aspect-ratio wrapper from static feature image, use <code>max-h-[480px] object-contain<\/code> for natural image proportions<\/li>\n<li><strong>FIX<\/strong> <code>checkout.blade.php<\/code>: add missing <code>billing_same_as_shipping<\/code> hidden field; remove stray coupon tag icon; use <code>$storeProductPriceText<\/code> and <code>$installationPriceText<\/code><\/li>\n<li><strong>MOD<\/strong> Creativa: <code>Coban303Ad<\/code> smart tracking scene starts at frame 180 (s6) instead of 270 (s9); add <code>TripHistoryScene<\/code> and <code>EndCard<\/code> components<\/li>\n<li><strong>FIX<\/strong> PayPal: read webhook body via <code>$request-&gt;getContent()<\/code> (stream already consumed by Laravel), fix broken token caching (<code>Cache::remember<\/code> + inner <code>Cache::put<\/code> conflict replaced with explicit get\/put using PayPal <code>expires_in<\/code> TTL), remove <code>PaymentCompletedEvent<\/code> dispatch from <code>CaptureController<\/code> to prevent double-fire (webhook-only), apply atomic <code>WHERE status != COMPLETED<\/code> guard in <code>ProcessPayPalEventJob<\/code> for idempotency, log only safe identifiers after signature verification (no PII), remove dead <code>$when<\/code> constructor param<\/li>\n<li><strong>FIX<\/strong> Meta CAPI: capture <code>clientIp<\/code>, <code>clientUserAgent<\/code>, <code>sourceUrl<\/code> from <code>request()<\/code> at HTTP dispatch time and propagate through event classes and listeners to <code>MetaConversionApi<\/code> (fixes null values in queue worker context), remove non-standard <code>CartAbandoned<\/code> CAPI event (Meta handles retargeting via InitiateCheckout gap), use <code>Number::defaultCurrency()<\/code> in <code>completeRegistration<\/code>, <code>lead<\/code>, and <code>schedule<\/code> instead of hardcoded EUR<\/li>\n<li><strong>FIX<\/strong> CRM <code>VisitorEventController<\/code> rate limit: add <code>throttle:20,1<\/code> middleware to <code>crm.visitor.event<\/code> route<\/li>\n<li><strong>FIX<\/strong> Remove MongoDB analytics infrastructure: delete <code>AnalyticsService<\/code>, <code>EventTypeEnum<\/code>, <code>RecordAnalyticEventJob<\/code>, <code>DailyAnalyticsCaptureCommand<\/code>, <code>MonthlyAnalyticsCaptureCommand<\/code>, <code>EventResource<\/code> (Filament), analytics policy, and migration; strip analytics dispatch calls from <code>LoginAction<\/code>, <code>PasswordForgotAction<\/code>, <code>RecordLoginListener<\/code>, <code>AuthServiceProvider<\/code>, and <code>DashboardStatsWidget<\/code><\/li>\n<li><strong>MOD<\/strong> Meta Conversion API: extract <code>MetaConversionResult<\/code> value object, enrich event payloads with <code>client_ip_address<\/code> and <code>client_user_agent<\/code> from request, add <code>SendMetaCartAbandonedConversionListener<\/code>, <code>SendMetaSubscribeConversionListener<\/code>, <code>SendMetaLeadConversionListener<\/code>, and <code>SendMetaCompleteRegistrationConversionListener<\/code>; wire to <code>CartAbandonedEvent<\/code>, <code>SubscriptionCreatedEvent<\/code>, <code>LeadVerifiedEvent<\/code>, and <code>NewUserEvent<\/code> in <code>EventServiceProvider<\/code>; add <code>test_event_code<\/code> config support in <code>AppServiceProvider<\/code><\/li>\n<li><strong>ADD<\/strong> CRM: <code>VisitorEventController<\/code> for client-side event ingestion, <code>crm-tracking<\/code> Blade partial to inject tracking script, <code>LeadAttributionMiddleware<\/code> extended with new activity types, <code>VisitorActivityTypeEnum<\/code> extended<\/li>\n<li><strong>ADD<\/strong> pSEO: comparison pages (<code>ComparisonEnum<\/code>, <code>ComparisonController<\/code>, <code>comparisons\/show.blade.php<\/code>) with BreadcrumbList and FAQPage schema, registered in <code>PSeoServiceProvider<\/code><\/li>\n<li><strong>ADD<\/strong> Demo booking events: <code>DemoBookingCreatedEvent<\/code>, <code>DemoBookingCreatedEvent\/<\/code> listener directory, wire in <code>EventServiceProvider<\/code>; add store event classes (<code>StoreOrderPlacedEvent<\/code>, <code>StoreProductViewedEvent<\/code>, <code>StoreProductAddedToCartEvent<\/code>, <code>StoreProductCheckoutStartedEvent<\/code>) with corresponding listener directories<\/li>\n<li><strong>ADD<\/strong> Device new store flow: <code>DeviceNewController<\/code> store step, <code>device\/new\/store.blade.php<\/code> view, <code>Api\/Device\/Order\/StoreController<\/code> with <code>index<\/code>\/<code>store<\/code>\/<code>products<\/code> endpoints, register routes in <code>api.php<\/code><\/li>\n<li><strong>MOD<\/strong> Store product descriptions: render markdown in store product pages (<code>store.product.show<\/code> and <code>store.product.landing<\/code>) using prose wrapper with image constraints; update <code>SellerSeeder<\/code> with benefit-focused markdown descriptions (feature screenshots, feature highlights, <code>\/features<\/code> link) for all Dakar and Toronto Traxelio products; use local <code>docs\/content\/products\/coban-303\/<\/code> images for Coban GPS303 and TK303G; add 4 new Toronto products (Bouncie GPS Tracker, LandAirSea 54, Tracki Pro, PlanetGPS OBD 4G)<\/li>\n<li><strong>ADD<\/strong> Product catalog JSON feed at <code>\/feed\/products.json<\/code> (<code>ProductCatalogFeedController<\/code>)<\/li>\n<li><strong>MOD<\/strong> Order lifecycle timestamps: rename <code>fulfilled_at<\/code> to <code>processed_at<\/code> (internal atomic lock), add <code>paid_at<\/code>, <code>preparing_at<\/code>, <code>ready_at<\/code>, <code>delivered_at<\/code>, <code>refunded_at<\/code>, <code>returned_at<\/code> columns; backfill old orders; add <code>currentStatus()<\/code> and <code>isPhysical()<\/code> to Order model; rename <code>OrderFulfillmentListener<\/code> to <code>OrderProcessedListener<\/code> and <code>OrderFulfillmentException<\/code> to <code>OrderProcessingException<\/code>; digital order handlers now set <code>delivered_at<\/code>; <code>STORE_GPS<\/code> (physical) skips auto-delivery; add order status tree card to <code>order.show<\/code> sidebar; add <code>OrderStatusEnum<\/code> with labels and colors<\/li>\n<li><strong>ADD<\/strong> PayPal as payment method for USD and CAD orders: <code>PayPalClientInterface<\/code>, <code>PayPalClient<\/code> (OAuth2 token caching, order creation, capture, webhook signature verification), <code>CaptureController<\/code> (return URL handler), <code>ProcessPayPalEventJob<\/code> (idempotent <code>PAYMENT.CAPTURE.COMPLETED<\/code> handler), webhook route, <code>checkout.paypal.capture<\/code> route, <code>PAYPAL<\/code> case in both <code>PaymentMethodEnum<\/code> enums, <code>fromCurrency()<\/code> returns Card+PayPal for USD and CAD, <code>CreateCheckoutUrlAction<\/code> PayPal block, <code>config\/services.php<\/code> paypal entry<\/li>\n<li><strong>MOD<\/strong> Normalize order items: subscription and installation line items now carry <code>product_type<\/code>\/<code>product_id<\/code> morph fields, <code>OrderFulfillmentListener<\/code> and <code>ProvisionDeviceAndSubscriptionListener<\/code> resolve plan and duration from item data (with fallback to <code>order.data<\/code> for legacy orders), morph map updated to use app-level <code>SubscriptionPlan<\/code> model, <code>CheckoutControllerTest<\/code> fixed with proper payload fields and address assertions<\/li>\n<li><strong>ADD<\/strong> Store product ad landing page (<code>store.product.landing<\/code>): distraction-free layout, <code>noindex<\/code> meta, Plausible <code>store-landing-buy-gps<\/code> events in header and form CTA<\/li>\n<li><strong>ADD<\/strong> Product rating field: <code>rating<\/code> float column on products and store_products tables, dynamic star rendering component (full\/half\/empty), AggregateRating schema.org markup, seed Cashier products with real review-based ratings<\/li>\n<li><strong>ADD<\/strong> Dashboard: SubscriptionHealthChart and UnitEconomicsChart widgets, make all chart widgets lazy-loadable, add currency filter to OrdersRevenueChart, add DashboardChartsSeeder<\/li>\n<li><strong>MOD<\/strong> POI computation action, command, notification, and tests<\/li>\n<li><strong>MOD<\/strong> CRM News module: update models, Filament resource, remove legacy migrations, update tests<\/li>\n<li><strong>MOD<\/strong> Subscription edit page cleanup<\/li>\n<li><strong>ADD<\/strong> Milestones Timeline Remotion composition with animated timeline nodes<\/li>\n<li><strong>MOD<\/strong> Config, TestCase, README cleanup, remove unused postcss.config.cjs<\/li>\n<li><strong>MOD<\/strong> Update TODO and planning docs, remove completed todo files<\/li>\n<li><strong>MOD<\/strong> OpenClaw: switch primary text model from Claude Sonnet ($3\/$15 per 1M tokens) to Gemini 2.0 Flash ($0.10\/$0.40) with Sonnet fallback, switch primary image model from DALL-E 3 to Nano Banana Pro with DALL-E fallback, add Gemini 2.5 Flash Lite for heartbeat pings, add Google API key to setup script and secrets<\/li>\n<li><strong>ADD<\/strong> Remotion carousel slide compositions (7 slides: hook, agitate, feature, how-it-works, benefit, social proof, CTA) with Instagram 1080x1080 format, Kill Switch as first carousel, render script for PNG export<\/li>\n<li><strong>MOD<\/strong> OpenClaw: update carousel-image-generator skill to prefer Remotion stills over DALL-E 3, update remotion-renderer skill with carousel still support, update batch-planner and docs<\/li>\n<li><strong>ADD<\/strong> Customer data interaction brainstorm document with 8 feature ideas (Fleet Pulse Terminal, Driver Scorecard, Trip Replay, Wrapped Stories, Live Delivery Tracker, Geofence Heatmap, NL Queries, Report Builder)<\/li>\n<li><strong>ADD<\/strong> Feb 2026 Week 1 product update Remotion composition with 5 scenes (intro, new pages, checkout, WhatsApp alerts, SEO\/CTA), registered in Root.tsx under Product-Updates folder<\/li>\n<li><strong>MOD<\/strong> POI detection blog posts (EN + FR): fix broken image references, add device settings, sensitivity config, and home screen POI screenshots<\/li>\n<li><strong>ADD<\/strong> One-click subscription renewal via web: signed URL page with duration\/payment picker, no login required. Notifications (email, push, WhatsApp) now link to this page instead of app deep links<\/li>\n<li><strong>ADD<\/strong> OpenClaw integration: CloudFormation template, deploy script actions, bundle script, 5 skills (batch-planner, caption-generator, changelog-to-content, remotion-renderer, carousel-image-generator), setup script, and documentation<\/li>\n<li><strong>ADD<\/strong> Run <code>db:seed --force<\/code> on every deploy (CI staging\/production, Makefile receive, preview) so new seeders apply automatically<\/li>\n<li><strong>MOD<\/strong> Make all seeders idempotent (<code>Fake\\DatabaseSeeder<\/code> switched from <code>create<\/code> to <code>firstOrCreate<\/code> with existence guards) so <code>db:seed<\/code> is safe to re-run<\/li>\n<li><strong>ADD<\/strong> Geotab vs Traxelio blog post (EN + FR) with USA\/Canada focus and New Jersey POC mention<\/li>\n<li><strong>MOD<\/strong> Fact-check Geotab vs Traxelio blog post: correct OEM count, hardware pricing, marketplace stats, FIPS detail, add inline CTAs, CTA section, and sourced references<\/li>\n<\/ul>\n<h3>Week 3 \u00b7 Feb 15-21<\/h3>\n<ul>\n<li><strong>ADD<\/strong> Dashboard: Returning Customers Rate stat per currency showing repeat buyer percentage<\/li>\n<li><strong>MOD<\/strong> Dashboard: Active Users chart now uses CRM visitor data (MongoDB) instead of legacy analytics, with blue line styling and y-axis config<\/li>\n<li><strong>MOD<\/strong> Dashboard: API Calls By Users chart now aggregates from MongoDB visitor_activities instead of legacy analytics_events<\/li>\n<li><strong>MOD<\/strong> Dashboard: Orders Revenue chart now groups revenue per currency (separate bar datasets for USD, XOF) with per-currency description<\/li>\n<li><strong>ADD<\/strong> Dashboard: Lead Funnel widget showing enrolled, email verified, and qualified stages with drop-off percentages and date filter support<\/li>\n<li><strong>ADD<\/strong> Dashboard: Earning per Visit and Sales Frequency stat cards per currency, with <code>CrmAnalyticsService::countVisits()<\/code> for date-filtered MongoDB visit counts<\/li>\n<li><strong>MOD<\/strong> Rewrite Privacy Policy and Terms of Service pages in English with <code>__()<\/code> translation wrappers, add third-party service provider categories, cookies, data retention, GPS tracking consent clause, and French translations<\/li>\n<li><strong>MOD<\/strong> Heatmap: extract <code>HeatmapService<\/code>, add time range filter to dashboard widget, add tests and French translations<\/li>\n<li><strong>ADD<\/strong> Fleet heatmap: authenticated API endpoint (<code>\/api\/heatmap<\/code>), Filament admin page, and mobile app home section with GPS position density visualization from MongoDB<\/li>\n<li><strong>MOD<\/strong> LandingStats: query trips, distance, and engine hours from MongoDB instead of MySQL<\/li>\n<li><strong>FIX<\/strong> OrdersRevenueChart: cast quantity to SIGNED in SQL multiplication to prevent BIGINT UNSIGNED overflow when computing revenue<\/li>\n<li><strong>ADD<\/strong> Sync events to MongoDB: real-time sync via <code>SyncEventToMongoDbJob<\/code>, backfill via <code>mongodb:feed-events<\/code>, indexes via <code>mongodb:init<\/code><\/li>\n<li><strong>ADD<\/strong> Browser, OS, device type, and UTM (source\/medium\/campaign) table filters on visitor resource<\/li>\n<li><strong>FIX<\/strong> Allow GPS order fulfillment without a device (hardware-only sales no longer throw an exception)<\/li>\n<li><strong>ADD<\/strong> MongoDB backup support (<code>--include-mongodb<\/code> flag on <code>db:backup<\/code>), enabled on daily scheduled backup<\/li>\n<li><strong>ADD<\/strong> Upcoming follow-ups widget on company list page<\/li>\n<li><strong>MOD<\/strong> Company status filter defaults to exclude &quot;Lost&quot;<\/li>\n<li><strong>ADD<\/strong> WhatsApp button on lead\/company tables, edit pages, and call modal to share presentation PDF<\/li>\n<li><strong>MOD<\/strong> Hide follow-up date field and skip follow-up creation when interest is cold\/dead<\/li>\n<li><strong>ADD<\/strong> Paginated POI visit endpoints (<code>poi\/{poi}\/visit<\/code>, <code>device\/{device}\/poi\/{poi}\/visit<\/code>, <code>team\/{team}\/poi\/{poi}\/visit<\/code>) with lat\/lng from position relationship<\/li>\n<li><strong>MOD<\/strong> Show company\/lead name in call action modal heading<\/li>\n<li><strong>FIX<\/strong> Deploy: stop sharing node_modules across releases (platform mismatch), use npm instead of yarn, fix null created_at in call form, BIGINT overflow in revenue chart, max() array bug in funnel widget<\/li>\n<li><strong>MOD<\/strong> Activity: enable auto created_at timestamps, add due_at\/completed_at columns to tables, replace completed_at DateTimePicker with toggle<\/li>\n<li><strong>MOD<\/strong> Makefile: reload php8.3-fpm after deploy\/rollback to clear OPcache<\/li>\n<li><strong>FIX<\/strong> Visitor-&gt;user() BelongsTo crash: same cross-database MongoDB-to-MySQL connection issue as Lead model<\/li>\n<li><strong>FIX<\/strong> Lead-&gt;user() BelongsTo crash: cross-database relation inherited MongoDB connection instead of MySQL, explicitly set default connection on User query<\/li>\n<li><strong>FIX<\/strong> ListCompanies search crash: MongoDB Builder's <code>search()<\/code> overrides scopeSearch, inline the where\/orWhere clauses instead<\/li>\n<li><strong>FIX<\/strong> MigrateCrmToMongoCommand: use <code>insert()<\/code> and <code>getMongoDB()-&gt;dropCollection()<\/code> for laravel-mongodb v5 compatibility<\/li>\n<li><strong>FIX<\/strong> RecordAnalyticEventJob: guard against missing <code>user_agent<\/code> key in data array<\/li>\n<li><strong>FIX<\/strong> Resolve all 21 PHPStan errors: type-hint Filament ownerRecord in RelationManagers, fix dead funnels() call in LeadController, fix PaymentController null access, correct Builder imports in CRM models, clean stale baseline entries<\/li>\n<li><strong>ADD<\/strong> <code>make health<\/code> command: full production health check covering system (disk, memory, load), services (nginx, php-fpm, mysql, redis, supervisor workers), app (HTTP, artisan, cache), WebSocket (Reverb on port 8080), and recent log errors<\/li>\n<li><strong>FIX<\/strong> Post-MongoDB migration code review: remove broken stage-mismatch cancellation in SkipIfUnsubscribed, stale $livewire ref in LeadResource, clear email_batch_id in UnsubscribeAllAction, fix migration command memory with chunked pivot queries and --fresh flag, fix CaptchaInput signature, clean stale PHPStan baseline<\/li>\n<li><strong>FIX<\/strong> EnrichCompanyFromWebsiteAction crash when metadata is a string instead of array<\/li>\n<li><strong>FIX<\/strong> Scheduled backup command crash: pass boolean flags as plain strings instead of key-value pairs to avoid Symfony &quot;option does not accept a value&quot; error<\/li>\n<li><strong>FIX<\/strong> Replace deprecated <code>-&gt;collection()<\/code> with <code>-&gt;table()<\/code> on MongoDB connection across 7 files (laravel-mongodb v5 compatibility)<\/li>\n<li><strong>MOD<\/strong> Rename GeofenceController to GeoZoneController with updated route binding<\/li>\n<li><strong>ADD<\/strong> Visitor-to-user linking on payment: save visitor fingerprint at checkout, then link visitor to buyer (User) on payment success for full purchase attribution<\/li>\n<li><strong>ADD<\/strong> CRM VisitorTest suite with CleansMongoDb trait for MongoDB test isolation<\/li>\n<li><strong>MOD<\/strong> Replace ClickHouse with MongoDB for analytics, CRM (Visitor\/Visit\/VisitorActivity), and position archival. CRM models now use MongoDB as primary store via <code>mongodb\/laravel-mongodb<\/code>. Remove <code>smi2\/phpclickhouse<\/code>, all ClickHouse services, observers, commands, Docker container, and config. Add MongoDB init\/migrate commands.<\/li>\n<li><strong>MOD<\/strong> Merge <code>docker-compose.yml<\/code> and <code>docker-compose.prod.yml<\/code> into a single file using Docker Compose profiles (<code>dev<\/code>, <code>prod<\/code>), delete <code>docker-compose.prod.yml<\/code>, update CI workflow<\/li>\n<li><strong>FIX<\/strong> Geocoding 162K rate-limit errors: merge Traccar configs, switch to Nominatim geocoder with reuseDistance=100 and cacheSize=10000<\/li>\n<li><strong>ADD<\/strong> POI <code>name<\/code> column and update endpoints at all 3 scope levels (user, team, device) with validation, authorization, and permission migration<\/li>\n<li><strong>FIX<\/strong> Reverb WebSocket crashes: add 512m memory limit, health check, logging, remove direct port exposure, proxy through nginx with proper WebSocket headers, reduce ping interval to 25s for Cloudflare compatibility<\/li>\n<li><strong>ADD<\/strong> Sysctl hardening (disable ICMP redirects, martian logging) and journald size cap (500M) to <code>rotate-logs<\/code> deploy action<\/li>\n<li><strong>ADD<\/strong> ClickHouse integration for analytics, CRM visitor tracking, and position archival with Docker container, config, Laravel services, init\/migration\/backfill commands, observers, and dashboard widget updates<\/li>\n<li><strong>ADD<\/strong> Version-controlled fail2ban configs under devops\/fail2ban (filters, jails, README)<\/li>\n<li><strong>ADD<\/strong> Traccar port probe detection jail (traccar-probe) banning scanners sending non-GPS data<\/li>\n<li><strong>FIX<\/strong> traccar-web-login filter: remove overly broad regex that was banning legitimate GPS trackers<\/li>\n<li><strong>ADD<\/strong> Mailcow mail server AWS infrastructure: CloudFormation template, S3 backup script, deploy.sh actions, and Makefile SSM targets<\/li>\n<li><strong>FIX<\/strong> EventResource null resource guard to prevent 500 error when event model is null<\/li>\n<li><strong>FIX<\/strong> product_shipping_zones migration foreign key constraint by using foreignUlid for ULID column type\/collation match<\/li>\n<li><strong>ADD<\/strong> Fail2ban jails for nginx scanning patterns (wp-scan, sensitive-files, php-scan, upload-scan, laravel-scan)<\/li>\n<li><strong>ADD<\/strong> Cloudflare real IP restoration in nginx config for accurate logging and fail2ban blocking<\/li>\n<li><strong>ADD<\/strong> Deployment rollback and release listing targets in Makefile, with automatic old release cleanup<\/li>\n<li><strong>ADD<\/strong> CriticalErrorNotification email alert for unhandled production exceptions (throttled per error)<\/li>\n<li><strong>ADD<\/strong> Mail server management targets in Makefile (mail-status, mail-update, mail-restart, mail-logs)<\/li>\n<li><strong>MOD<\/strong> Database backups from weekly to daily with compression and single-transaction flags<\/li>\n<li><strong>MOD<\/strong> Fail2ban DB purge age from 1d to 7d to prevent stale ban accumulation<\/li>\n<li><strong>MOD<\/strong> Dockerfile: add mysql-client for database backup support<\/li>\n<li><strong>FIX<\/strong> WhatsAppNotification model table name mismatch (Laravel resolved <code>whats_app_notifications<\/code> instead of <code>whatsapp_notifications<\/code>)<\/li>\n<li><strong>FIX<\/strong> AppointmentStatusCommand queried non-existent <code>verified_at<\/code> column, changed to <code>confirmed_at<\/code><\/li>\n<li><strong>FIX<\/strong> Order <code>scopeUnpaid<\/code> BIGINT UNSIGNED overflow by casting price\/quantity to SIGNED<\/li>\n<li><strong>FIX<\/strong> Landing stats blade undefined array keys with null coalescing defaults<\/li>\n<li><strong>FIX<\/strong> Docker build: install Node.js via nvm from <code>.nvmrc<\/code> to fix <code>npm ci<\/code> platform mismatch<\/li>\n<li><strong>FIX<\/strong> Resolve PHPStan errors in OrdersRevenueChart, CartController, GeoZoneController, and AuthServiceProvider<\/li>\n<li><strong>ADD<\/strong> Team-scoped geo-zone CRUD API endpoints (index, store, show, update, destroy) with policy, permissions seeder, and migration to grant geo-zone permissions to existing team members<\/li>\n<li><strong>MOD<\/strong> Frontend: redesign product landing page, update header\/head\/trackers\/profile views, add SCSS styles, update JS\/CSS assets, Creativa map presets, and Filament map vendor views<\/li>\n<li><strong>MOD<\/strong> Refactor CountryEnum (replace Countries helper), update PlanEnum, TrackerDevice model, DbLeanCommand, service providers, and composer dependencies<\/li>\n<li><strong>MOD<\/strong> Cart and checkout: CartIcon livewire component, cart view overhaul, guest cart merge on login, address migration and model updates, marketplace checkout alignment<\/li>\n<li><strong>MOD<\/strong> POI: cursor pagination for GetPoisAction, scoped device visit sums, user\/team show+destroy endpoints, Filament filters\/widgets, PlacesService, observer, factories, seeder, and team POI permissions migration<\/li>\n<li><strong>FIX<\/strong> Add missing <code>TeamPoiPolicy<\/code> import in <code>AuthServiceProvider<\/code> and register <code>poi.view<\/code>\/<code>poi.delete<\/code> gates<\/li>\n<li><strong>MOD<\/strong> Rename <code>BlogPostSeeder<\/code> to <code>PostSeeder<\/code><\/li>\n<li><strong>MOD<\/strong> Apple-inspired checkout redesign: standalone pages, clean typography, WCAG-compliant contrast, brand color active states, shipping\/returns policy links<\/li>\n<li><strong>ADD<\/strong> User-scoped POI <code>show<\/code> and <code>destroy<\/code> API endpoints (<code>GET \/api\/poi\/{id}<\/code>, <code>DELETE \/api\/poi\/{id}<\/code>) with <code>PoiPolicy<\/code> authorization<\/li>\n<li><strong>ADD<\/strong> Team-scoped POI API endpoints (index, show, destroy) for fleet-wide POI management with <code>TeamPoiPolicy<\/code> authorization<\/li>\n<li><strong>ADD<\/strong> POI sensitivity preset per device (low\/medium\/high) with <code>PoiSensitivityEnum<\/code>, per-device settings via API, grouped computation in <code>ComputePoisAction<\/code>, default to high sensitivity<\/li>\n<li><strong>FIX<\/strong> <code>Device\\PoiController::index<\/code> queried POIs by device ownership (always empty); now queries via visits. Added missing <code>device.poi.view_any<\/code> gate<\/li>\n<li><strong>FIX<\/strong> <code>Api\\PoiController<\/code> silently ignored invalid <code>device_id<\/code>; added <code>exists<\/code> and <code>after_or_equal<\/code> validation<\/li>\n<li><strong>ADD<\/strong> Tests for <code>PoiController<\/code> (14), <code>Device\\PoiController<\/code> (7), and <code>DeviceTest<\/code> POI relationships (3)<\/li>\n<li><strong>MOD<\/strong> Checkout: shipping zone shown in order summary with live price update, removed address from customer card on order show<\/li>\n<li><strong>MOD<\/strong> Cart checkout: dynamic shipping line in order summary (zone label + price), estimated total updates live as shipping zone changes, <code>shipping_zone<\/code> required on backend validation<\/li>\n<li><strong>ADD<\/strong> Installation service as optional linked cart item: toggle on product landing page, installation flows through as a regular cart item with linked quantity sync and cascade delete, checkout handles installation items natively with backward compat fallback<\/li>\n<li><strong>ADD<\/strong> Visitor funnel widget on admin dashboard: shows conversion stages (visit, product view, lead, purchase) with drop-off percentages and overall conversion rate over last 7 days<\/li>\n<li><strong>MOD<\/strong> Lead create page: premium redesign with two-column layout, frosted glass card, trust badges, 3-step onboarding flow, and responsive video embed<\/li>\n<li><strong>FIX<\/strong> Duplicate FAQPage structured data: remove duplicate <code>@stack('head')<\/code> from all layout files (already called inside <code>element.head<\/code>)<\/li>\n<li><strong>ADD<\/strong> Address model in cashier package with structured fields (street, apartment, city, state, zip, country, lat\/lng). Orders now use <code>shipping_address_id<\/code>\/<code>billing_address_id<\/code> FK instead of text columns<\/li>\n<li><strong>MOD<\/strong> Address form: autocomplete on street field fills all structured fields from Google Place Details API, GeoIP auto-fills city\/region\/country<\/li>\n<li><strong>MOD<\/strong> Remove ShippingZoneEnum entirely: shipping zone resolution now uses <code>Address::resolveShippingZone()<\/code> matching against <code>ShippingZone<\/code> model records (city &gt; region &gt; country priority), <code>shipping_zone<\/code> stored as plain string on orders<\/li>\n<li><strong>MOD<\/strong> Checkout flow: session-based product checkout (landing POSTs to prepare, redirects to GET checkout), shared <code>checkout.fields<\/code> Blade component for shipping\/billing\/payment, cart checkout page (<code>cart.checkout.show<\/code>) reusing same component<\/li>\n<li><strong>MOD<\/strong> Shipping zones: move from <code>data.shipping_zones<\/code> JSON to dedicated <code>product_shipping_zones<\/code> DB table with <code>Product\\ShippingZone<\/code> model and <code>ReturnCostTypeEnum<\/code> in cashier package, product currency instead of user currency on landing page, config zones as default fallback<\/li>\n<li><strong>ADD<\/strong> Product landing page (<code>product.landing<\/code>): conversion-focused page for paid\/organic ad campaigns, stripped of navigation distractions, with all plans (including free), shipping + billing address, optimized meta tags (OG, Twitter Card, Schema.org, image preload), visitor tracking integration (PRODUCT_VIEWED activity, 10-point route score), and FR translations<\/li>\n<li><strong>ADD<\/strong> Configurable shipping zones (<code>config\/traxelio.shipping<\/code>): zone-based flat-rate shipping (GTA free, Ontario $10, Canada $15, USA $25) with delivery time estimates, dynamic zone resolution on shipping address input via Alpine.js<\/li>\n<li><strong>ADD<\/strong> MerchantReturnPolicy and ShippingService structured data (JSON-LD) to returns and shipping pages<\/li>\n<li><strong>ADD<\/strong> Shipping and return policy pages with FAQ accordion sections, FAQPage JSON-LD schema, and FR translations<\/li>\n<li><strong>ADD<\/strong> ECR-based Docker deployment: CI builds and pushes image to ECR, deploy pulls pre-built image instead of building on instance; new <code>docker-compose.prod.yml<\/code> for production (no local mysql\/redis), entrypoint script for shared nginx volume, <code>APP_ENV<\/code> build arg in Dockerfile, ECR repository in CloudFormation with lifecycle policies, optional <code>uv<\/code> extension install<\/li>\n<li><strong>MOD<\/strong> Landing stats placeholder: use actual icons and labels with skeleton pulse on number only<\/li>\n<li><strong>MOD<\/strong> Roaming zones: add Zone 2 countries (CI, BJ, ML, BF, TN, FR, CH, BE, LU, PT, IL, US, AF), Zone 4 (13 countries, 6053 XOF), and Zone 5 (6 countries, 12107 XOF)<\/li>\n<li><strong>FIX<\/strong> Authenticate middleware: avoid serializing Eloquent model in afterResponse closures to prevent ModelNotFoundException on deleted tokens<\/li>\n<li><strong>MOD<\/strong> POI feature: move to premium plan, add plan check to SettingPoiTrackingPolicy, replace PoiData\/PoiVisitData DTOs with PoiResource\/PoiVisitResource HTTP resources<\/li>\n<li><strong>ADD<\/strong> Creativa: POI Detection concept video with clustered stop visualization, stats panel, and notification<\/li>\n<li><strong>MOD<\/strong> POI computation: move tunable parameters (cluster radius, min stop duration, lookback days) to <code>config('traxelio.poi')<\/code><\/li>\n<li><strong>MOD<\/strong> POI domain: introduce typed DTOs (StopData, ClusterData, ComputePoisResult) replacing associative arrays in ComputePoisAction and ComputePoisCommand<\/li>\n<li><strong>ADD<\/strong> Device POI API: show and delete routes for premium+ customers with plan-based authorization, app_url deep linking for mobile<\/li>\n<li><strong>ADD<\/strong> POI detection notifications: opt-in per-device POI_TRACKING setting with controller, policy, and API routes; NewPoisDetectedNotification via email and push; admin monitoring notifications; 24h rate limiting per user<\/li>\n<li><strong>MOD<\/strong> ComputePoisAction: replace delete-recreate with upsert strategy (match existing POIs within 100m radius), return <code>['total' =&gt; int, 'new' =&gt; int]<\/code> for new POI detection<\/li>\n<li><strong>ADD<\/strong> POI_TRACKING feature page in FeatureEnum with label, description, benefits, use cases, and FAQs<\/li>\n<li><strong>ADD<\/strong> Separate product checkout page (<code>product.checkout<\/code>): shipping address, billing address, payment method, order summary with dynamic shipping zone resolver, split from landing page for cleaner UX<\/li>\n<li><strong>ADD<\/strong> Full email verification flow: VerifyEmailNotification, verify-email view, verification routes, and PromptEmailVerification listener dispatch; remove default Laravel Registered\/SendEmailVerificationNotification mapping<\/li>\n<li><strong>ADD<\/strong> PR preview deployments: ephemeral preview environments on AWS for each PR at <code>pr-{number}.traxelio.com<\/code>, with auto-deploy on CI pass and auto-destroy on PR close\/merge; reuses staging ALB, VPC, and instance profile; IAM policies for EC2, ALB, and SSM scoped to preview resources<\/li>\n<li><strong>FIX<\/strong> Replace closure-based middleware in base Listener with serializable SuppressModelNotFound class to fix SQS serialization error<\/li>\n<li><strong>ADD<\/strong> POI (Point of Interest) detection: precomputed weekly clustering of frequent vehicle stop locations from ignition\/device events, morphic ownership (User or Team), 100m greedy centroid-based clustering with 2-min minimum stop filter, API endpoint with device and date range filtering, <code>pois:compute<\/code> artisan command with <code>--user<\/code> and <code>--team<\/code> options<\/li>\n<li><strong>FIX<\/strong> POI computation: handle unclosed stops (device still stopped at computation time), chunk event queries by device for memory efficiency, add cache lock to prevent concurrent execution, add error handling in batch command<\/li>\n<li><strong>ADD<\/strong> UserSeeder with N. Aissa Dia as sales role user, registered in DatabaseSeeder<\/li>\n<li><strong>MOD<\/strong> Post model: support slug-only route binding with fallback to legacy slug-id format<\/li>\n<li><strong>MOD<\/strong> Blog posts (FR\/EN): replace &quot;temps reel&quot;\/&quot;real time&quot; with &quot;en direct&quot;\/&quot;live&quot;, convert EUR to USD+FCFA pricing, add 3 FAQ questions for subscription upsell, split CTA links one per line<\/li>\n<li><strong>ADD<\/strong> Store product page: blog post link in free plan callout<\/li>\n<li><strong>MOD<\/strong> ActivityResource: use <code>actor.name<\/code> relationship with eager loading instead of N+1 <code>User::find()<\/code>, add entity_type filter with <code>getMorphClass()<\/code><\/li>\n<li><strong>ADD<\/strong> Activity model: <code>actor()<\/code> BelongsTo relationship to User<\/li>\n<li><strong>MOD<\/strong> Rename <code>roaming_zones.country_code<\/code> to <code>country<\/code> across migration, model, seeder, and report job<\/li>\n<li><strong>ADD<\/strong> ActivityDirectionEnum with HasLabel, HasColor, HasIcon for typed direction field on Activity model<\/li>\n<li><strong>MOD<\/strong> Replace TextInput with ToggleButtons for activity direction in ActivityResource and both ActivitiesRelationManagers<\/li>\n<li><strong>MOD<\/strong> Move CRM resource access control from inline canAccess() to CRMPolicy registered via gates in CRMServiceProvider<\/li>\n<li><strong>ADD<\/strong> Roaming zones: RoamingZone model with YAS Senegal pricing (28 countries across 3 zones), subscription data column for roaming countries, roaming cost added to checkout pricing via getPriceFor(), PriceController response includes roaming breakdown, AdminBusinessReport roaming section<\/li>\n<li><strong>MOD<\/strong> Product item component: render as non-clickable div when cta is false<\/li>\n<li><strong>ADD<\/strong> Enrich companies from website: scrape emails, phone numbers (from tel: links), external links, and fleet\/logistics contacts (from team\/about pages via JSON-LD or HTML parsing) into metadata during enrichment<\/li>\n<li><strong>ADD<\/strong> CompanyResource: related records section showing matched Lead, User, and Team with view links<\/li>\n<li><strong>ADD<\/strong> CompanyResource: filters for website, phone, and city<\/li>\n<li><strong>MOD<\/strong> Standardize 4 listeners to extend base Listener class<\/li>\n<li><strong>MOD<\/strong> Rewrite checkConverted: distinguish CONVERTED (phone match with devices) from ENRICHED (phone\/email match without devices or existing lead)<\/li>\n<li><strong>ADD<\/strong> CRM company enrichment: scrape website metadata, add companies table metadata column, enrich listener with website action<\/li>\n<li><strong>MOD<\/strong> Listeners: standardize base Listener usage, company resource filters and related records<\/li>\n<li><strong>MOD<\/strong> Appointment and company resource improvements<\/li>\n<li><strong>ADD<\/strong> CRM Activity system: polymorphic <code>crm_activities<\/code> table with <code>HasActivities<\/code> trait on Company and Lead models, ActivityTypeEnum (call, email, sms, meeting, note), ActivityResource, and ActivitiesRelationManager on both Company and Lead edit pages<\/li>\n<li><strong>ADD<\/strong> Call action on Company and Lead resources (table + edit page): modal with dial button, last called timestamp, interest rating (hot\/warm\/cold\/dead), summary, and follow-up scheduling. Auto-updates company status and unsubscribes dead leads from sales funnel<\/li>\n<li><strong>ADD<\/strong> Outreach checklist on Company edit page: collapsible section with auto-checked pre-call items, during-call guidance, and post-call CRM update reminders<\/li>\n<li><strong>MOD<\/strong> Refactor UnsubscribeAction to accept FunnelEnum instead of Funnel model<\/li>\n<li><strong>MOD<\/strong> Optimize PHP-FPM for production: static PM with 30 children, custom <a href=\"http:\/\/www.conf\">www.conf<\/a>, JIT enabled, OPcache timestamp validation disabled, memory_limit reduced to 256M, secure session cookies, FPM status\/ping endpoints, and <code>php artisan optimize<\/code> in deploy pipeline<\/li>\n<li><strong>ADD<\/strong> Blog post &quot;Traceur GPS sans abonnement&quot; (FR + EN): honest comparison of subscription vs subscription-free GPS trackers with cost analysis<\/li>\n<li><strong>ADD<\/strong> Standalone \/how-it-works page with premium design: trust badges, step-by-step installation guide, tracker type cards, FAQ section<\/li>\n<li><strong>MOD<\/strong> Homepage how-to-start section: add &quot;See the full guide&quot; link to \/how-it-works page<\/li>\n<li><strong>FIX<\/strong> Remove duplicate FAQPage structured data from 9 templates (pricing, how-it-works, tracker index\/brand\/protocol, location city\/country, homepage FAQ, ROI calculator) to comply with Google's FAQ schema guidelines<\/li>\n<li><strong>FIX<\/strong> Catch ModelNotFoundException during middleware construction in base Job and Listener classes using template method pattern (jobMiddleware\/listenerMiddleware)<\/li>\n<li><strong>ADD<\/strong> Sales role with CRM panel access: sales users can view, create, and edit all CRM resources (Company, Lead, Appointment, Visitor, Stage, Funnel, NewsArticle, NewsQuery) but cannot delete records<\/li>\n<li><strong>ADD<\/strong> CI gate job and concurrency controls to GitHub Actions workflow for PR checks on main<\/li>\n<\/ul>\n<h3>Week 2 \u00b7 Feb 8-14<\/h3>\n<ul>\n<li><strong>MOD<\/strong> Expand GooglePlacesTypeEnum to 32 valid Table A types covering fleet, logistics, field service, food, transportation, and corporate businesses<\/li>\n<li><strong>MOD<\/strong> Cast Company country as CountryEnum, update CompanyResource with searchable select and proper label formatting<\/li>\n<li><strong>FIX<\/strong> Replace invalid Google Places types (truck_rental, logistics_service, taxi_stand) with valid Table A types<\/li>\n<li><strong>MOD<\/strong> Remove duplicate fulfillment transaction from OrderFulfillmentListener, RecordDeviceSales is now the single source via AppointmentCompletedEvent<\/li>\n<li><strong>ADD<\/strong> Order appointments relationship and AppointmentsRelationManager on OrderResource<\/li>\n<li><strong>ADD<\/strong> &quot;Mark Completed&quot; action on EditAppointment page that sets serviced_at and dispatches AppointmentCompletedEvent<\/li>\n<li><strong>MOD<\/strong> Remove GPS_SOLD analytics event type (unused business metric)<\/li>\n<li><strong>FIX<\/strong> RecordDeviceSales: add idempotency guard, eager load relationships, avoid unnecessary device model load for morph class<\/li>\n<li><strong>FIX<\/strong> AppointmentResource API: use client_type column directly instead of lazy-loading client relationship<\/li>\n<li><strong>FIX<\/strong> Remove deleted BackfillConversionActivitiesCommand from CRM service provider<\/li>\n<li><strong>FIX<\/strong> PHPStan fixes: typed $record on EditNewsArticle\/EditVisitor, typed bulk action loop in VisitorResource, Device position link, higher-order proxy calls, Visitor platform docblock, StoreProduct route binding, NewsArticle newsjack_score<\/li>\n<li><strong>MOD<\/strong> Switch SearchNearbyBusinessesAction from searchNearby to searchText API with paginated generator and rate-limited page fetching<\/li>\n<li><strong>ADD<\/strong> CompanyResource Filament page for CRM prospecting with form sections, status badges, filters, and global search<\/li>\n<li><strong>ADD<\/strong> NewCompanyCreatedEvent with EnrichNewCompanyListener: reverse geocode city\/country via Nominatim, auto-tag as converted if phone matches existing user\/team\/device<\/li>\n<li><strong>MOD<\/strong> SearchCompaniesCommand: add --save flag to persist results, map GooglePlacesTypeEnum to IndustryEnum<\/li>\n<li><strong>MOD<\/strong> Company model: cast industry to IndustryEnum, move conversion check from inline to event-driven listener<\/li>\n<li><strong>MOD<\/strong> Centralize Alpine.js and collapse plugin imports in element\/head.blade.php, remove duplicate CDN script tags from 21 view files and 3 components<\/li>\n<li><strong>MOD<\/strong> Redesign order show page with premium card styling, status badges, breadcrumb nav, dynamic shipping\/discount\/grand total, elevated payment form for unpaid orders, and polished customer sidebar<\/li>\n<li><strong>FIX<\/strong> Abandoned cart flow not triggering: buyer-less orders (external\/taxi deposits) crashed the command with TypeError, added <code>whereNotNull('buyer_id')<\/code> filter<\/li>\n<li><strong>MOD<\/strong> Refactor abandoned cart command to fire CartAbandonedEvent, move batch creation to SendAbandonedCartSequenceListener with Slack notification<\/li>\n<li><strong>MOD<\/strong> Stop pruning unfinished batches so 29-day abandoned cart email sequences keep batch context<\/li>\n<li><strong>ADD<\/strong> Homepage above-fold-home variant with product screenshot hero<\/li>\n<li><strong>ADD<\/strong> Visitor resource filters: country, city, user, user agent, fingerprint<\/li>\n<li><strong>ADD<\/strong> Record conversion activities (lead_created, email_verified, account_created, qualification_started, qualification_completed) from event listeners and FindOrCreateLeadAction, with backfill command for existing leads<\/li>\n<li><strong>ADD<\/strong> Visitor blocking system: BlockedVisitor model with cached UA pattern and IP blocklist, integrated into TrackVisitorMiddleware, with Filament admin actions to manage entries<\/li>\n<li><strong>ADD<\/strong> BlogPostSeeder: add public status page announcement post (EN + FR pair) covering uptime monitoring infrastructure, 19 monitors across 4 groups, and transparency positioning<\/li>\n<li><strong>ADD<\/strong> Auto-create public status page with grouped monitors (Core, Marketing, pSEO, DNS) in setup-kuma.py<\/li>\n<li><strong>FIX<\/strong> Add custom header to Uptime Kuma HTTP monitors to bypass Cloudflare bot detection<\/li>\n<li><strong>ADD<\/strong> Uptime monitors for pSEO indexes, blog, login, marketing pages, and DNS resolution checks (13 new monitors, 19 total) to setup-kuma.py<\/li>\n<li><strong>ADD<\/strong> Uptime monitors for \/up health check, \/pricing, \/demo, and \/sitemap.xml to setup-kuma.py<\/li>\n<li><strong>ADD<\/strong> Auto-setup Uptime Kuma on deploy: setup-kuma.py script fetches admin credentials from AWS Secrets Manager, creates admin user (or logs in), and adds HTTP monitors for traxelio.com and app.traxelio.com idempotently; IAM policy for Secrets Manager access, integrated into bootstrap.sh, deploy.sh, and deploy-status.yml CI workflow<\/li>\n<li><strong>ADD<\/strong> News monitoring system: NewsQuery\/NewsArticle models, migrations, factories, GoogleNewsFetcher service, FetchNewsCommand, and tests<\/li>\n<li><strong>ADD<\/strong> Per-package test scripts and autoload entries for actions, captcha, expo-notification-channel, export, tags, things-mobile, whatsapp-channel<\/li>\n<li><strong>ADD<\/strong> Expand news monitoring queries to US, Canada, UK, Australia, and New Zealand markets, consolidate redundant Senegal queries (29 to 18), total 45 queries across 6 countries<\/li>\n<li><strong>MOD<\/strong> Redesign contact page with premium hero, 4 contact cards (support, sales, Dakar phone, US phone), office section with map, dark CTA, and structured data<\/li>\n<li><strong>MOD<\/strong> Redesign user profile page with dark gradient hero, premium account details and vehicle cards with status badges, and app download banner<\/li>\n<li><strong>MOD<\/strong> Footer contact link now points to \/contact page instead of WhatsApp, green pulse dot links to status.traxelio.com<\/li>\n<li><strong>MOD<\/strong> Upgrade staging EC2 from t3.medium to t3.large (8 GB RAM)<\/li>\n<li><strong>FIX<\/strong> Decouple status page infrastructure from main stack to allow independent provisioning<\/li>\n<li><strong>ADD<\/strong> News digest email notification to admin after daily fetch when new articles are found, with per-query breakdown<\/li>\n<li><strong>ADD<\/strong> Status page at status.traxelio.com with uptime monitoring, SSL, daily backups, and IP-restricted admin<\/li>\n<li><strong>ADD<\/strong> OIDC-based CI\/CD authentication with environment-scoped least-privilege roles<\/li>\n<li><strong>MOD<\/strong> Harden infrastructure security: secret management, network restrictions, HTTPS with TLS 1.3, monitoring alarms, and least-privilege deployments<\/li>\n<li><strong>MOD<\/strong> Make StoreProduct slug non-unique, use self-healing routes (slug-id format) so multiple stores can share the same product slugs<\/li>\n<li><strong>MOD<\/strong> Link RecordDeviceSales transaction to order fulfillment: remove hardcoded SIM cost, use actual appointment costs with itemized description, set entity to device, store fulfillment_transaction_id on linked order<\/li>\n<li><strong>ADD<\/strong> RSS 2.0 feed at <code>\/feed<\/code> for LinkedIn auto-posting: FeedController serves blog posts and pSEO enum content (guides, glossary, features) as XML, with <code>&lt;link rel=&quot;alternate&quot;&gt;<\/code> discovery tag in head<\/li>\n<li><strong>FIX<\/strong> Resolve all fixable PHPStan errors without baseline: remove baseline file, fix 108 errors across 67 files (139 to 31 remaining). Fixes include: Eloquent $attributes object\/array conflicts via setAttribute(), Filament form\/record type narrowing with @property-read and @param PHPDoc, scope method @method annotations on 8 models, pivot access refactored to getAttribute(), Collection generic typing, DB::raw parameter fix, sprintf bug in Point, ActivityReport property name bug, dead code removal, CurrencyEnum type mismatches, Promise callback types, and Tag relation return type<\/li>\n<li><strong>FIX<\/strong> Resolve PHPStan errors in app\/ codebase: fix type annotations, nullable types, int casts on math functions, Request::query defaults, @property PHPDoc prefixes, Filament record type narrowing, and PositionPolicy param typo; reduce baseline from 163 to 140 suppressed errors<\/li>\n<li><strong>FIX<\/strong> Resolve PHPStan errors across all 11 lib\/ packages: fix undefined methods, type mismatches, missing @property annotations, and unqualified class references<\/li>\n<li><strong>MOD<\/strong> Unify monorepo tooling: root <code>composer analyze\/lint\/test<\/code> now covers all lib\/ packages, add <code>composer.json<\/code> with per-package scripts to all 11 lib\/ packages, remove redundant cashier-specific and <code>:all<\/code> composite scripts from root<\/li>\n<li><strong>MOD<\/strong> Split PHPStan into per-package configs: independent <code>phpstan.neon<\/code> for each of the 11 <code>lib\/<\/code> packages, root config now app-only, new <code>composer analyze:{package}<\/code> and <code>analyze:all<\/code> scripts<\/li>\n<li><strong>ADD<\/strong> Cart system for multi-product orders: polymorphic cart items with price\/currency\/metadata, CartController (show\/store\/update\/destroy), cart checkout creating Order with multiple OrderItems, guest cart merge on login, CRM add_to_cart tracking, cart icon in header with item count badge, &quot;Add to Cart&quot; button on product page alongside existing &quot;Buy now&quot; direct checkout<\/li>\n<li><strong>MOD<\/strong> Rename <code>itemable_type<\/code>\/<code>itemable_id<\/code> to <code>product_type<\/code>\/<code>product_id<\/code> on order_items for consistency with cart_items polymorphic relationship<\/li>\n<li><strong>MOD<\/strong> Consolidate order migrations into <code>lib\/cashier\/<\/code> package: merge 6 alter-table migrations (itemable, data, coupon, shipping, lat\/lng, billing_address) into single <code>create_orders_table<\/code> migration, move <code>itemable()<\/code> MorphTo and <code>data<\/code> cast from app OrderItem to cashier base, move <code>scopePaid<\/code>\/<code>scopeUnpaid<\/code> and <code>idShort<\/code> to cashier Order, enable <code>loadMigrationsFrom<\/code> in CashierServiceProvider, update docblocks on both Order and OrderItem models<\/li>\n<li><strong>ADD<\/strong> Link CRM visitor to user on login: pass cookie fingerprint through LoginEvent to new LinkVisitorOnLoginListener, also pass fingerprint to LinkVisitorToLeadJob in InviteNewUserForDemo so web visitors are linked via the lead path<\/li>\n<li><strong>MOD<\/strong> Move Coupon model, DiscountTypeEnum, CouponFactory, and coupon-related Order attributes (coupon(), discountedTotal, grandTotal) to <code>lib\/cashier\/<\/code> package, with root app extending the cashier base; marketplace controllers now use config-driven coupon lookup<\/li>\n<li><strong>MOD<\/strong> Move <code>studios-neptune\/cashier<\/code> from VCS dependency to local <code>lib\/cashier\/<\/code> package: remove VCS repo\/require from composer.json, add PSR-4 autoload entries, register CashierServiceProvider in bootstrap\/providers.php, add Cashier test suite to phpunit.xml, adapt test namespaces and factory references for root app context<\/li>\n<li><strong>ADD<\/strong> Polymorphic <code>itemable<\/code> on order items: GPS device order items are now linked to their source StoreProduct via morphTo\/morphMany relationship, with composite index on <code>itemable_type<\/code>\/<code>itemable_id<\/code><\/li>\n<li><strong>ADD<\/strong> Reusable <code>x-input.plan-picker<\/code> Blade component: extracts plan selector cards from store product checkout into a shared component with configurable label, name, default, recommended badge, description, and price display; replaces inline HTML in store\/product\/show and product\/show<\/li>\n<li><strong>ADD<\/strong> Checkout success (thank-you) page: order confirmation summary with items, subtotal, shipping, discount, and grand total; conditional appointment booking CTA when order includes installation; auto-generated 10% coupon for next purchase<\/li>\n<li><strong>ADD<\/strong> Appointment booking for store product installation: date\/time slot picker with address pre-fill from order, creates Appointment linked to order, auth-protected routes<\/li>\n<li><strong>ADD<\/strong> Coupon system: DiscountTypeEnum (percent\/fixed), Coupon model with validation (expiry, max uses, user restriction, min amount), 10% auto-generated coupon for repeat buyers, coupon code input on checkout form, Filament CouponResource with admin policy<\/li>\n<li><strong>ADD<\/strong> Flat-rate shipping zones: ShippingZoneEnum with haversine distance from Dakar center (5km city, 20km suburbs, other regions), zone-based shipping costs (XOF 2000\/3000\/5000, USD 3\/5\/8), shipping cost and zone stored on orders, grand total calculation<\/li>\n<li><strong>ADD<\/strong> Installation opt-out toggle on store product checkout: buyers can uncheck professional installation even when the product requires it, with Alpine.js toggle defaulting to checked, hidden input for form submission, and conditional order item creation<\/li>\n<li><strong>ADD<\/strong> Delivery address field with Alpine.js autocomplete: new <code>x-input.address<\/code> Blade component fetching suggestions from <code>\/api\/place<\/code>, integrated into store product checkout form with debounced search and dropdown selection<\/li>\n<li><strong>ADD<\/strong> Seller dashboard access: store owners can now access the Filament admin panel to manage their stores and products, with query scoping to show only their own data, policies for Store and StoreProduct models, and admin-only <code>canAccess()<\/code> guards on all other Filament resources<\/li>\n<li><strong>ADD<\/strong> Free plan tier for store product checkout: $0\/0 XOF for 1 year per device with limited tracking (3-day trip history, 1 session, no sharing\/teams\/API\/support), auto-upgrades to Basic on expiry; hidden from pricing page, device setup flow, and API; includes gift icon SVG component, Alpine.js duration picker toggle, and paidCases() helper to exclude FREE from non-store contexts<\/li>\n<li><strong>MOD<\/strong> Switch store product files from unconfigured <code>store-products<\/code> disk to <code>stores<\/code> disk with hierarchical paths (<code>{storeId}\/products\/<\/code>), add file cleanup on Store and StoreProduct deletion via <code>deleteDirectory()<\/code>, update Filament resource directory paths and seeder<\/li>\n<li><strong>MOD<\/strong> StoreProduct multi-media support: rename <code>image<\/code> to <code>featured_media<\/code> with new <code>medias<\/code> JSON column for gallery images\/videos, consolidate 4 marketplace migrations into 2 clean create-table migrations, add <code>mediaUrls<\/code> accessor for signed temporary URLs, update Filament resource with multiple file upload, download real product images in seeder<\/li>\n<li><strong>ADD<\/strong> Seller storefront product catalog, detail, and checkout pages: <code>\/s\/{store}<\/code> short URL, <code>\/store\/{store}\/product<\/code> index with product selection and checkout sidebar, <code>\/store\/{store}\/product\/{storeProduct}<\/code> detail page with related products, scoped route bindings, out-of-stock display support, stock\/published guards on checkout<\/li>\n<li><strong>MOD<\/strong> Replace Seller with User in Marketplace package: remove standalone Seller model\/table in favor of App\\Models\\User, add CanSellProducts trait, rename store_products.image_url to image (with Storage-based computed URL), rename store_products.specs to data, move StoresRelationManager to UserResource, update factories\/seeder\/tests<\/li>\n<\/ul>\n<h3>Week 1 \u00b7 Feb 1-7<\/h3>\n<ul>\n<li><strong>MOD<\/strong> Make \/guides pages informational and less salesy: add educational\/product category system to GuideEnum with dynamic badges, remove &quot;Why Choose Traxelio&quot; promotional section from HOW_TO_CHOOSE guide, neutralize comparison table highlighting and FAQ self-promotion, remove hero CTA buttons from all guide pages, replace bottom CTA with &quot;Next Steps&quot; (educational) or soft &quot;Need More Help?&quot; (product), restructure index page with grouped categories and &quot;What You'll Find&quot; section, update meta title year to 2026, add French translations for all new keys<\/li>\n<li><strong>FIX<\/strong> Alpine.js missing on learn, features\/show, guides\/show, and location pages: add Alpine.js CDN script (and collapse plugin where FAQ accordions are used) so search\/filter on \/learn and FAQ accordions work<\/li>\n<li><strong>MOD<\/strong> Replace inline button styles with reusable <code>x-button.primary<\/code> and <code>x-button.secondary<\/code> components across 16 files (~35 button conversions): hero CTAs, bottom CTA sections, and standalone buttons on features, industries, trackers, learn, guides, and locations pages; standardizes border-radius, font-weight, and hover states<\/li>\n<li><strong>MOD<\/strong> Redesign GPS tracking country\/city pages: replace inline Mapbox hero with <code>hero-map-bg<\/code> component accepting country\/city props, extract 6 shared partials (popular-industries, faq, cta, payment-ecosystem, infrastructure, location-styles) from duplicated code in country.blade.php and city.blade.php, redesign payment ecosystem as full-row layout with progress bars and PaymentMethodEnum-based integration detection, redesign infrastructure as unified card with coverage progress bars; enrich Abidjan with neighborhoods, local insights, payment ecosystem, and infrastructure data; add 4 new CI cities (Bouake, Yamoussoukro, San Pedro, Korhogo) with full French content; add transportation and construction to CI popular industries<\/li>\n<li><strong>ADD<\/strong> Customer success feature email sequence: 12 new notification classes educating customers about key features (low battery, power cut, geo-zones, speed limits, access sharing, driving behaviors, reports, email reports, documents, cash flow, upgrade to premium, switch to yearly); extends existing 7-email sequence to 18 emails over 45 days with plan-gated Premium nudges for Basic users and conditional skip middleware for commercial emails<\/li>\n<li><strong>MOD<\/strong> Replace ImportLocationsCommand with LocationSeeder: convert artisan command to seeder with file-by-file processing, memory cleanup, and created\/updated\/skipped counters; remove command registration from PSeoServiceProvider<\/li>\n<li><strong>ADD<\/strong> Seller Marketplace package (<code>lib\/marketplace\/<\/code>): third-party GPS device sellers with stores and products, seller storefront page (<code>\/seller\/{seller}<\/code>), checkout flow reusing existing Order\/Payment pipeline with new <code>SELLER_GPS<\/code> order target, Filament admin resources for Sellers and Store Products, noindex for seller pages, seeder with initial seller data, French translations<\/li>\n<li><strong>FIX<\/strong> \/learn page search and category filter broken by apostrophes in term descriptions: move term metadata into a JSON object via Js::from() to safely escape special characters, replace inline x-show expression with isVisible() helper<\/li>\n<li><strong>MOD<\/strong> Split location data files: countries.json split by continent (africa.json, europe.json, asia.json, north-america.json, south-america.json), cities.json split by country code (SN.json, FR.json, etc.) for easier maintenance<\/li>\n<li><strong>ADD<\/strong> GPS Tracker Troubleshooting Guide: new guide covering battery\/power issues, connection problems, location accuracy, and alerts not working; includes FAQ section with 5 common questions and French translations<\/li>\n<li><strong>ADD<\/strong> low battery FAQ to Installation Guide: explains vehicle battery health, charging while driving, and technician support option<\/li>\n<li><strong>ADD<\/strong> SecumorePlus (26 devices) and AKSH (4 devices) GPS tracker brands to import list<\/li>\n<li><strong>ADD<\/strong> YouTube and TikTok social icons to footer<\/li>\n<li><strong>MOD<\/strong> Global city pSEO enhancement: enrich all 133 city descriptions with source-cited statistics; add port authority citations (major port cities), traffic congestion data (INRIX, city transport agencies), vehicle registration stats, and logistics market data across Africa (18 cities), Europe (45 cities), Americas (32 cities), and Asia-Pacific (38 cities); embed &quot;(Source: [Authority Year])&quot; inline citations across descriptions and challenges; 409 total Source: citations now in cities.json<\/li>\n<li><strong>FIX<\/strong> N+1 queries on <code>\/api\/user\/{user}\/device<\/code>: always eager-load <code>position<\/code> relation since DeviceResource unconditionally accesses position-dependent attributes, add <code>team<\/code> to optional relations allowlist<\/li>\n<li><strong>FIX<\/strong> allow admin to bypass <code>after:now<\/code> validation on appointment <code>scheduled_at<\/code> field, accept ISO 8601 datetime format without timezone<\/li>\n<li><strong>MOD<\/strong> Global country pSEO enhancement: enrich 37 country descriptions with source-cited statistics matching USA quality; add Transport Canada ELD citations (Canada), Eurostat\/national statistics (Europe 5 countries), port authority data (Latin America 6 countries), government ministry citations (Asia-Pacific 11 countries), African Development Bank and port statistics (Africa 14 countries); embed specific trade values, port throughput, fleet sizes, and regulatory dates with year references; 115 new &quot;Source: [Authority Year]&quot; citations across descriptions and challenges<\/li>\n<li><strong>ADD<\/strong> Fleet ROI Calculator at \/tools\/fleet-roi-calculator: interactive Alpine.js calculator targeting &quot;gps tracking roi calculator&quot; BOFU keyword, input fields for fleet size, fuel\/maintenance costs, vehicle value, idle time, unauthorized use, and GPS tracking cost; outputs monthly savings breakdown (fuel, maintenance, theft prevention, productivity), net ROI, annual savings, payback period, and annual ROI percentage; FAQPage structured data with 4 ROI questions, educational content section, CTA to demo; French translations for all labels<\/li>\n<li><strong>FIX<\/strong> tracker pages: add missing Alpine.js collapse plugin to brand and show pages for FAQ accordions, sort pricing plans cheapest to most expensive on device show page, remove Alarm from Alerts &amp; Notifications section<\/li>\n<li><strong>FIX<\/strong> pricing page FAQ accordion not working: add missing Alpine.js and collapse plugin scripts<\/li>\n<li><strong>MOD<\/strong> remove Traccar mentions from all public pages: pricing page (FAQ, meta, comparison heading), trackers index FAQ, guides enum examples, remove 4 Traccar-branded devices from tracker-devices.json<\/li>\n<li><strong>ADD<\/strong> regulatory content to location pages: add regulations JSON column to countries table with migration, add regulations section to country.blade.php showing legal status, employee consent requirements, relevant laws, and data storage requirements; add regulations field to all 19 countries in countries.json with country-specific laws (NDPR, POPIA, GDPR, CNIL, etc.); add French translations for regulation keys; update LocationSeeder and Country model to handle regulations cast<\/li>\n<li><strong>MOD<\/strong> pricing page performance: add HTTP cache headers (1h browser, 24h CDN) to pricing endpoint, 24h database cache for device\/brand counts in pricing page and footer<\/li>\n<li><strong>MOD<\/strong> move funnel migrations to lib\/crm package: consolidate funnels and funnel_stages migrations into lib\/crm\/database\/migrations, update funnels table to use ULID primary key and add email_batch_id column to funnel_lead pivot<\/li>\n<li><strong>FIX<\/strong> location pages JSON-LD: escape FAQ question\/answer values with json_encode() to prevent XSS and structured data parsing errors<\/li>\n<li><strong>FIX<\/strong> sitemap scalability: use chunk(500) instead of each() for Country\/City queries to prevent memory issues at scale<\/li>\n<li><strong>ADD<\/strong> French translations for location pages: 25+ new strings for GPS tracking, network coverage, transport challenges, industries, and city navigation<\/li>\n<li><strong>ADD<\/strong> feature tests for LearnController and GuideController: index and show pages, 404 handling, breadcrumbs, FAQs, related terms, checklist verification<\/li>\n<li><strong>FIX<\/strong> glossary search box: unify Alpine.js scope so search input filters term cards by label and description<\/li>\n<li><strong>ADD<\/strong> sitemap integration for \/learn and \/guides pages: add GlossaryTermEnum and GuideEnum URLs to SitemapController<\/li>\n<li><strong>MOD<\/strong> header dark mode: use Tailwind dark: variants for nav links and logo switching, scroll background adapts to theme (bg-white\/95 light, bg-gray-900\/95 dark), listens for system preference changes<\/li>\n<li><strong>MOD<\/strong> footer redesign (Laravel-inspired): use Tailwind dark: variants for automatic light\/dark theme based on system preference, remove CTA card for cleaner navigation-focused layout, move social icons under tagline, add Resources column (Blog, Buyer's Guide, Glossary), move Privacy\/Terms to bottom section inline with copyright, simplify trust signal to device count only, remove gradient accent line and background glow, keep &quot;Built by B.C.D&quot; credit<\/li>\n<li><strong>ADD<\/strong> Buyer's Guide and Educational Hub: GlossaryTermEnum (12 GPS\/fleet concepts and technical terms), GuideEnum (How to Choose GPS Tracking buyer's guide), \/learn glossary index and term pages with category filtering, \/guides index and detailed guide pages with interactive checklist, comparison tables, pricing models, platform types, decision checklist, FAQ sections with structured data, 60+ French translations<\/li>\n<li><strong>MOD<\/strong> footer redesign: premium SaaS aesthetic with darker bg-gray-950, gradient accent line, trust signals (uptime + device count), CTA card with WhatsApp link, consolidated links (removed Login and Our Products), refined social icons (LinkedIn\/Twitter first), cleaner bottom layout<\/li>\n<li><strong>MOD<\/strong> trackers index cleanup: remove feature marketing sections (Driving Behavior, Data Points, Alerts) to focus on device discovery, keeping Hero, Search, Featured Devices, Popular Brands, All Brands A-Z, FAQ, and CTA sections<\/li>\n<li><strong>FIX<\/strong> button hover state: fix primary button fading on hover by keeping text white, add x-button.primary, x-button.secondary, and x-button.dark Blade components with consistent styles and arrow icon<\/li>\n<li><strong>MOD<\/strong> pricing page simplification: remove 6-month billing option (keep Monthly and Yearly only), remove misleading &quot;Free mobile apps&quot; text since subscription is required per device<\/li>\n<li><strong>ADD<\/strong> SEO sections to tracker device show pages: &quot;Best Tracking Software&quot; section targeting &quot;[device] tracking software&quot; and &quot;[protocol] GPS server&quot; keywords, &quot;Compatible Apps&quot; section targeting &quot;[device] compatible app&quot; searches showing Traxelio web\/iOS\/Android platforms<\/li>\n<li><strong>MOD<\/strong> pricing page redesign: reorder plans Basic to Platinum (low to high), elevated Premium card with gradient badge, pill-style billing toggle, cleaner price display without decimals, only show selected billing cycle price, dark CTA buttons, add &quot;Not sure?&quot; links to demo and features pages below pricing cards<\/li>\n<li><strong>ADD<\/strong> BOFU optimization for \/pricing page: Cost Comparison section (vs self-hosted, enterprise, manufacturer apps), ROI\/Value section with savings stats (fuel, theft recovery, maintenance, safety), 3 new BOFU FAQs targeting &quot;cheapest GPS tracking&quot; and &quot;is GPS tracking worth it&quot; keywords, Product\/Offer structured data for rich search results, enhanced meta description<\/li>\n<li><strong>ADD<\/strong> dedicated \/pricing page targeting BOFU keywords: billing cycle toggle with savings display (8% for 6 months, 17% for yearly), value proposition section, FAQPage structured data with 6 pricing FAQs, trust signals showing device\/brand counts, currency-aware pricing via Number::currency(), link added to footer and homepage pricing section<\/li>\n<li><strong>MOD<\/strong> store brand logos on disk: rename brand_logo_url column to brand_logo (stores filename), add tracker-brands S3 disk, update accessor to return 15-minute signed temporary URL, import command downloads Clearbit logos and stores them on disk<\/li>\n<li><strong>ADD<\/strong> featured_image column to tracker_devices: stores manufacturer product images on S3 trackers disk with 15-minute signed URLs via getFeaturedImageUrlAttribute(), FeaturedTrackerDeviceSeeder downloads images from AliExpress\/Teltonika\/Queclink\/Jimilab<\/li>\n<li><strong>ADD<\/strong> BOFU optimization for tracker pages: FAQ sections with FAQPage structured data on \/trackers index and \/trackers\/{brand} pages, trust bar and &quot;Why Traxelio&quot; section on device show pages, featured devices highlight on brand pages with star badge and price range, BOFU FAQ (&quot;Is Traxelio better than the official app?&quot;) added to TrackerDevice::getFaqs()<\/li>\n<li><strong>FIX<\/strong> WhatsApp API error when credentials not configured: add early validation in SendWhatsAppMessageAction to throw descriptive exception instead of cryptic &quot;Object with ID 'messages'&quot; Graph API error<\/li>\n<li><strong>ADD<\/strong> enrich tracker index page with 4 new sections: driving behavior analysis (Traxelio-exclusive), featured devices with price ranges and category tabs, data points collected with SVG icons, and alerts\/notifications split into standard and smart alerts. Add getSvgIcon()\/isSmartAlert() to Event\/TypeEnum, price_min\/price_max columns to tracker_devices, and FeaturedTrackerDeviceSeeder<\/li>\n<li><strong>MOD<\/strong> tracker pages polish: replace &quot;\/month\/device&quot; with &quot;per month per device&quot;, add A-Z letter nav and Alpine.js brand filter in All Brands section, add ~40 French translations for tracker index and show pages<\/li>\n<li><strong>ADD<\/strong> brand logo URL (Clearbit) and device image URL columns to tracker devices, with import command auto-population for ~50 known brands, brand logos in device cards, device\/brand images in show page hero<\/li>\n<li><strong>MOD<\/strong> replace hardcoded vehicle SVG icons with Blade Icons @svg() directive using DeviceCategoryEnum::getIcon()<\/li>\n<li><strong>ADD<\/strong> GPS Trackers link to footer Product column<\/li>\n<li><strong>ADD<\/strong> 4 new industry pages (Small Business Fleet, Motorcycle Security, Family Car Safety, Field Service) targeting untapped pSEO verticals with challenges, solutions, stats, and recommended features<\/li>\n<li><strong>ADD<\/strong> enrich tracker device show page: DataPointEnum (12 telemetry data points with protocol-based resolution), Data Collected \/ Detectable Events \/ Compatible Vehicles \/ Benefits sections, hero stat badges, 2 new dynamic FAQs in structured data, protocol-aware content (standard wired vs advanced vs unknown)<\/li>\n<li><strong>ADD<\/strong> WhatsApp channel to 20 transactional and device alert notifications: implement WhatsAppMessageInterface, add WhatsAppChannel to via(), add toWhatsApp() with concise plain-text messages for payment, appointment, subscription, geofence, speed, towing, offline, power cut, battery, curfew, trip, and braking notifications<\/li>\n<li><strong>ADD<\/strong> pSEO tracker device pages: 1,500+ GPS tracker pages at \/trackers\/{brand}\/{device} with index, brand, and device views, Traccar scraper import command (trackers:import), TrackerDevice model, structured data (BreadcrumbList, FAQPage, SoftwareApplication), sitemap integration, pricing sections, and feature cross-linking<\/li>\n<li><strong>ADD<\/strong> WhatsApp template message support: WhatsAppTemplateMessage with fluent builder for Meta template API, template_name column for querying\/analytics, storageBody() for human-readable logging<\/li>\n<li><strong>ADD<\/strong> WhatsApp notification channel: lib\/whatsapp-channel package using Meta Cloud API with queued jobs (3 retries, backoff), typed exceptions (auth, rate limit, invalid recipient), phone validation before dispatch, WhatsAppNotification model with ULID and 90-day pruning<\/li>\n<li><strong>MOD<\/strong> abandoned cart: create AbandonedCartNotification base class, refactor 4 email notifications to extend it, add SkipIfOrderPaidOrDeleted middleware, create SendAbandonedCartEmailSequenceJob, fix date range bug in command, add whereNull fulfilled_at filter, schedule command hourly<\/li>\n<li><strong>FIX<\/strong> reuse existing unfulfilled orders instead of creating duplicates on double-click\/retry in Product\/CheckoutController, Device\/SubscriptionController, and Device\/PaymentController; add throttle:checkout middleware to unprotected checkout routes<\/li>\n<li><strong>MOD<\/strong> magic login: allow multiple concurrent login codes per user by switching from single cache key to per-code cache keys<\/li>\n<li><strong>MOD<\/strong> merge device_id column alignment into original CREATE TABLE migrations, remove separate alignment migration<\/li>\n<li><strong>FIX<\/strong> device ID migration: disable FK checks to handle Traccar-created foreign keys on production<\/li>\n<li><strong>ADD<\/strong> GPS transfer page: customers from other providers can transfer their GPS to Traxelio (collect info, verify email, create device, continue to plan selection)<\/li>\n<li><strong>ADD<\/strong> BlogPostSeeder: add vehicle security blog post (prevent car\/scooter theft with GPS features), add French translation, add to seeder as FR\/EN pair<\/li>\n<li><strong>MOD<\/strong> remove volume discount from quote page, expand QuoteController tests (16 tests covering validation, order items, subscription line items, yearly discount, show method)<\/li>\n<li><strong>FIX<\/strong> volume discount tiers (10-99: 5%, 100-999: 15%), show subscription at full monthly price with separate &quot;months free&quot; discount line item for non-monthly billing<\/li>\n<li><strong>MOD<\/strong> extract lead creation logic from LeadController and QuoteController into FindOrCreateLeadAction, add unit tests<\/li>\n<li><strong>MOD<\/strong> redesign \/post listing page: premium header with CTA, collapsible search\/filters panel with tag chips, card grid with hover effects, featured first post, fixed broken images, placeholder SVG for missing thumbnails, premium empty state<\/li>\n<li><strong>MOD<\/strong> BlogPostSeeder: rewrite &quot;GPS Transforms Car Rental&quot; post (remove competitor examples, remove generic GPS explanation, add rental-specific scenarios, add internal links, add images, add feature table), add French translation, add to seeder as FR\/EN pair<\/li>\n<li><strong>MOD<\/strong> BlogPostSeeder: rewrite &quot;How GPS Trackers Can Save Your Life&quot; post (replace off-topic content with vehicle-focused safety scenarios, add internal links, add images, add feature table), add French translation, add to seeder as FR\/EN pair<\/li>\n<li><strong>MOD<\/strong> BlogPostSeeder: improve SinoTrack ST-901 product post (remove salesy filler, add specs table, add internal links, add images, add pricing table, integrate with Traxelio platform), add French translation, add to seeder as FR\/EN pair<\/li>\n<li><strong>MOD<\/strong> BlogPostSeeder: improve Benefits of GPS Tracking post (remove outline, add internal links, add images, add feature table, tighten prose), add French translation, add to seeder as FR\/EN pair<\/li>\n<li><strong>MOD<\/strong> BlogPostSeeder: improve Ultimate Guide to GPS Trackers post (remove TOC\/outline, add internal links, add images, add comparison table), add French translation, add to seeder as FR\/EN pair<\/li>\n<li><strong>FIX<\/strong> TrackApiCallMiddleware: rename <code>country<\/code> to <code>country_code<\/code> in deviceData to match TrackVisitorMiddleware and RecordVisitorActivityJob<\/li>\n<li><strong>MOD<\/strong> BlogPostSeeder: improve FR Score de Conduite post (remove emojis, add internal links, add report images, add industry links), add English translation, add to seeder as FR\/EN pair<\/li>\n<li><strong>MOD<\/strong> BlogPostSeeder: improve rental car GPS tracking post (tighten prose, add internal links, add images), add French translation, add to seeder as FR\/EN pair<\/li>\n<li><strong>MOD<\/strong> BlogPostSeeder: improve Driver Score post (remove emojis, add internal links, add report images), add French translation, add to seeder as FR\/EN pair<\/li>\n<li><strong>MOD<\/strong> BlogPostSeeder: improve Orange Money payment integration post, add English translation, add to seeder as FR\/EN pair<\/li>\n<li><strong>MOD<\/strong> BlogPostSeeder: migrate 4 French-only posts to paired FR\/EN format with English translations (meilleur-traceur-gps-senegal, comparatif-geolocalisation-flotte-dakar, gps-tracker-voiture-senegal-avis, meilleure-solution-suivi-flotte-afrique-ouest), remove getFrenchOnlyPosts() and seoPostContent() methods<\/li>\n<li><strong>ADD<\/strong> BlogPostSeeder: 4 French + 4 English SEO blog posts targeting price-sensitive queries (prix, combien ca coute, sans abonnement, pas cher) for the Senegalese GPS tracking market<\/li>\n<li><strong>ADD<\/strong> BlogPostSeeder: 6 French + 6 English SEO blog posts targeting transactional keywords for the Senegalese GPS tracking market, with translation linking and tag strategy<\/li>\n<li><strong>ADD<\/strong> HTTP tests for DeviceResource: verify full response structure, ignition_on logic per device model (s102a, st902, seaworld102 use motion), and no-position fallback<\/li>\n<li><strong>FIX<\/strong> DeviceTest: correct s102a ignition assertion to reflect motion-only logic<\/li>\n<li><strong>MOD<\/strong> replace generic rectangle-stack icons with contextual heroicons across 16 Filament resources<\/li>\n<li><strong>FIX<\/strong> customer success funnel: prevent duplicate email sequences when event fires multiple times<\/li>\n<li><strong>FIX<\/strong> visitor tracking: fix country_code never stored (wrong key name), null-safe geoData access, fix firstOrUpdate to updateOrCreate<\/li>\n<li><strong>ADD<\/strong> copyable URL column on visitor activities table<\/li>\n<li><strong>FIX<\/strong> appointment form: prefill scheduled_date via afterStateHydrated so date and time slot display on edit<\/li>\n<li><strong>MOD<\/strong> rename cost_type to type on appointment costs model, migration, enum defaults, and Filament resource<\/li>\n<li><strong>MOD<\/strong> show scheduled_at timestamp as fillable field on appointment resource status section<\/li>\n<li><strong>FIX<\/strong> API call tracker: capture country, city, device info, and timezone from Context and GeoIP<\/li>\n<li><strong>FIX<\/strong> visitor tracking: use UA-parsed device data from Context instead of Client Hints headers for reliable device_type, device_brand, device_os capture<\/li>\n<li><strong>MOD<\/strong> exclude map.image route from visitor tracking<\/li>\n<li><strong>ADD<\/strong> path-based exclusions for visitor tracking, exclude .well-known\/* paths<\/li>\n<li><strong>ADD<\/strong> French translations for appointment notification emails (reminder, assigned, confirmed, scheduled)<\/li>\n<li><strong>MOD<\/strong> appointment notification fallback strings wrapped in __() for translation support<\/li>\n<li><strong>MOD<\/strong> demo booking: improved error display with icon, network error detection, and slot conflict recovery link<\/li>\n<li><strong>MOD<\/strong> demo booking: skip captcha verification for authenticated users<\/li>\n<li><strong>MOD<\/strong> demo booking status pages (confirmed, cancelled, reschedule, rescheduled): redesigned with hero map background<\/li>\n<li><strong>ADD<\/strong> first-party visitor scoring system: anonymous web tracking, API tracking, engagement scores, composite fit\/engagement scores, Slack alerts on hot visitors<\/li>\n<li><strong>ADD<\/strong> visitors, visits, visitor_activities tables with migrations and factories<\/li>\n<li><strong>ADD<\/strong> VisitorScorer service, TrackVisitorMiddleware, TrackApiCallMiddleware, RecalculateVisitorScoresCommand<\/li>\n<li><strong>ADD<\/strong> VisitorResource and LeadResource VisitorsRelationManager in Filament admin<\/li>\n<li><strong>ADD<\/strong> configurable excluded_routes to filter noise (livewire.update) from visitor tracking<\/li>\n<li><strong>MOD<\/strong> remove unused MOBILE_APP platform, keep WEB and API only<\/li>\n<li><strong>MOD<\/strong> LeadResource: add composite_score and engagement_score columns<\/li>\n<li><strong>MOD<\/strong> Lead model: add visitors\/visits relationships and score casts<\/li>\n<li><strong>MOD<\/strong> enterprise page: card spread animation and overlay tweaks<\/li>\n<li><strong>ADD<\/strong> French translations for enterprise managed service section<\/li>\n<li><strong>FIX<\/strong> AppointmentConfirmationFlowTest: expect ModelNotFoundException instead of silent skip<\/li>\n<li><strong>MOD<\/strong> base Job classes skip ModelNotFoundException silently instead of marking as failed<\/li>\n<li><strong>ADD<\/strong> magic login link action on UserResource edit page (admin-only, 15-min expiry)<\/li>\n<li><strong>FIX<\/strong> UserResource: add key() to Lead Placeholder component to resolve action binding error<\/li>\n<li><strong>FIX<\/strong> demo booking page: display time slots in user's browser timezone instead of server timezone<\/li>\n<li><strong>ADD<\/strong> Seaworld 102 GPS device model with commands and port configuration<\/li>\n<li><strong>ADD<\/strong> Slack logging on captcha verification failures (success=false, action mismatch, low score)<\/li>\n<li><strong>ADD<\/strong> Lead link placeholder on UserResource form for quick navigation<\/li>\n<li><strong>MOD<\/strong> AppointmentResource: remove step precision on lat\/long, enable reminder_sent_at editing<\/li>\n<li><strong>FIX<\/strong> SendClientCalendarInviteListener: use findOrFail instead of find for safer lookup<\/li>\n<li><strong>MOD<\/strong> email header logo to use higher resolution image (logo_h60.png)<\/li>\n<li><strong>MOD<\/strong> remove unused Logging middleware<\/li>\n<li><strong>MOD<\/strong> remove obsolete create_leads_table migration (handled by CRM package)<\/li>\n<li><strong>ADD<\/strong> first-party visitor scoring system in lib\/crm\/: tracks anonymous and identified visitors across web, mobile app, and API with engagement scoring, time decay, and composite fit\/engagement scores<\/li>\n<li><strong>ADD<\/strong> visitors, visits, and visitor_activities tables with ULID keys, cascade deletes, and proper indexes<\/li>\n<li><strong>ADD<\/strong> engagement_score and composite_score columns to leads table<\/li>\n<li><strong>ADD<\/strong> VisitorScorer service with configurable point values, category caps, time decay (14-day half-life), recency bonuses, and negative signals<\/li>\n<li><strong>ADD<\/strong> TrackVisitorMiddleware for server-side web tracking via first-party cookie (_trx_vid)<\/li>\n<li><strong>ADD<\/strong> RecordVisitorActivityJob and RecalculateVisitorScoreJob for async activity recording and score computation<\/li>\n<li><strong>ADD<\/strong> VisitorScoreChangedEvent with AlertOnHotVisitorListener for Slack alerts when score thresholds are crossed<\/li>\n<li><strong>ADD<\/strong> RecalculateVisitorScoresCommand (visitors:recalculate-scores) scheduled daily at 02:00<\/li>\n<li><strong>MOD<\/strong> Logging middleware to track authenticated API\/app users as visitors<\/li>\n<li><strong>MOD<\/strong> CRMServiceProvider to auto-load migrations and register visitor scoring command<\/li>\n<li><strong>ADD<\/strong> VisitorResource Filament admin panel with visits and activities relation managers, score badges, device\/location\/attribution sections, and filters for hot\/warm\/active visitors<\/li>\n<li><strong>MOD<\/strong> LeadResource with composite_score and engagement_score columns, and VisitorsRelationManager to view linked visitors<\/li>\n<li><strong>ADD<\/strong> COMMAND_RESULT case with label and description to Event\/TypeEnum<\/li>\n<li><strong>ADD<\/strong> French translations for COMMAND_RESULT description and welcome account message<\/li>\n<\/ul>\n<h2>2026-01<\/h2>\n<ul>\n<li><strong>ADD<\/strong> appointment system showcase on enterprise page with screenshot collage and benefits list<\/li>\n<li><strong>ADD<\/strong> Appointment Scheduling row to features comparison table (premium and platinum tiers)<\/li>\n<li><strong>MOD<\/strong> align email theme colors with Tailwind config: success (green-600), error (rose-600), add secondary and warning button variants, branded top accent on email card, rounded panel corners<\/li>\n<li><strong>ADD<\/strong> success\/danger notifications to appointment confirm and cancel table actions<\/li>\n<li><strong>ADD<\/strong> appointments relation manager on DeviceResource with HasMany relation on Device model<\/li>\n<li><strong>ADD<\/strong> appointments relation manager on UserResource with clientAppointments MorphMany on User model<\/li>\n<li><strong>FIX<\/strong> remove ShouldQueue from DeviceObserver to fix force delete failing with ULID type mismatch error<\/li>\n<li><strong>MOD<\/strong> decouple appointment confirmation flow: client gets confirmation request on creation (not technician assignment), calendar invites sent individually on confirm\/assignment instead of waiting for both parties<\/li>\n<li><strong>ADD<\/strong> demo booking pending\/confirmed flow with signed URLs for confirm, cancel, and reschedule actions, plus French translations<\/li>\n<li><strong>MOD<\/strong> premium duration picker with card grid, check indicators, best value badge, and reactive plan prices<\/li>\n<li><strong>MOD<\/strong> fix quote show: use order data for fleet size, plan tier, and discount calculation<\/li>\n<li><strong>ADD<\/strong> required asterisk indicator on x-input.text, toggle-buttons, and plan-duration-picker components<\/li>\n<li><strong>ADD<\/strong> reCAPTCHA on quote form<\/li>\n<li><strong>MOD<\/strong> redesign quote form with premium styling, fleet-size input, plan toggle buttons, duration picker, and x-input Blade components<\/li>\n<li><strong>MOD<\/strong> simplify quote flow: rename proposal to show, redirect store to quote.show, use fleet_size + plan instead of product array<\/li>\n<li><strong>ADD<\/strong> French translations for all quote page strings<\/li>\n<li><strong>ADD<\/strong> structured data: Organization schema site-wide (moved from homepage to head partial), BreadcrumbList JSON-LD on blog post\/feature\/industry\/product pages, Event schema on demo booking page<\/li>\n<li><strong>MOD<\/strong> SEO quick wins: populate web manifest name fields, add preconnect\/dns-prefetch hints, add aria-labels to hero map and video thumbnail, serve robots.txt dynamically with correct content type<\/li>\n<li><strong>ADD<\/strong> billing duration support (monthly\/6-month\/annual) to quote creation with duration-based subscription pricing<\/li>\n<li><strong>ADD<\/strong> plan tier badge and annual billing comparison section to quote proposal view<\/li>\n<li><strong>ADD<\/strong> phone and address capture on quote submission (saved to user and order)<\/li>\n<li><strong>ADD<\/strong> data column to order_items table for storing plan_id and product_id metadata<\/li>\n<li><strong>MOD<\/strong> on \/demo with in-house booking calendar (Alpine.js date picker, time slots, booking form with email confirmation)<\/li>\n<li><strong>ADD<\/strong> enterprise landing page (\/enterprise) with enterprise messaging, social proof, and demo CTA<\/li>\n<li><strong>ADD<\/strong> demo booking page (\/demo) with Cal.com\/Calendly embed placeholder<\/li>\n<li><strong>ADD<\/strong> professional quote\/proposal PDF view with fleet summary, per-vehicle pricing, and volume discounts<\/li>\n<li><strong>ADD<\/strong> enterprise (200+) fleet size option to lead qualification flow with dedicated email sequence and Slack notification<\/li>\n<li><strong>MOD<\/strong> hot lead demo invitation email to link to \/demo instead of product.start<\/li>\n<li><strong>MOD<\/strong> lead bucket enum to support enterprise bucket for 200+ vehicle fleets<\/li>\n<li><strong>MOD<\/strong> defer Facebook Pixel via requestIdleCallback and move from head to body-end; defer Tawk.to until user interaction or 5s timeout<\/li>\n<li><strong>MOD<\/strong> blog index title and description to include location and niche keywords for SEO<\/li>\n<li><strong>ADD<\/strong> translated_post_id column to link blog post translations with cross-referencing hreflang tags and JSON-LD workTranslation<\/li>\n<li><strong>ADD<\/strong> hreflang tags and dynamic og:locale\/html lang for bilingual blog posts to prevent duplicate content issues<\/li>\n<li><strong>FIX<\/strong> og:url used url()-&gt;full() including query params, now uses url()-&gt;current() matching canonical<\/li>\n<li><strong>FIX<\/strong> product page thumbnail images missing alt text for accessibility and image search<\/li>\n<li><strong>FIX<\/strong> OG\/Twitter card meta tags: broken image fallback, hardcoded og:type, twitter:creator typo; add og:image dimensions and consistent og-default.png fallback<\/li>\n<li><strong>ADD<\/strong> meta descriptions to privacy policy and terms of use pages<\/li>\n<li><strong>ADD<\/strong> JSON-LD structured data: LocalBusiness (homepage), FAQPage (homepage FAQ), Product (product page), Article (blog post)<\/li>\n<li><strong>ADD<\/strong> meta descriptions to 6 public pages (blog, download, contact, support, founder letter, customers) with French translations<\/li>\n<li><strong>ADD<\/strong> dynamic sitemap.xml endpoint with static pages, industry, feature, post, and product URLs<\/li>\n<li><strong>MOD<\/strong> add payments to OrderResource and wrap response with data key<\/li>\n<li><strong>MOD<\/strong> remove api.user.order routes in favor of api.order<\/li>\n<li><strong>ADD<\/strong> api.order.show route for viewing single order<\/li>\n<li><strong>FIX<\/strong> CRM lead model binding to use configurable model class<\/li>\n<li><strong>ADD<\/strong> user appointments api: filter by device_ulid query parameter<\/li>\n<li><strong>ADD<\/strong> win-back email funnel for churned customers re-engagement (9-stage, 90-day sequence)<\/li>\n<li><strong>ADD<\/strong> crash detection with automatic alert notifications<\/li>\n<li><strong>ADD<\/strong> user metrics tracking with milestone events (10km, 100km, 1k, 10k, 100k, 1M km)<\/li>\n<li><strong>ADD<\/strong> customer success email funnel for post-purchase engagement<\/li>\n<li><strong>ADD<\/strong> device activation email sequence to push users through new device onboarding<\/li>\n<li><strong>ADD<\/strong> landing page: problems section to better explain value prop<\/li>\n<li><strong>ADD<\/strong> landing page: add app-overview mock up<\/li>\n<li><strong>ADD<\/strong> social login: apple, google<\/li>\n<li><strong>ADD<\/strong> CRM: lead scoring<\/li>\n<li><strong>ADD<\/strong> industry pages: taxi, VTC, car rental with custom layouts<\/li>\n<li><strong>MOD<\/strong> blog post page layout and styling improvements<\/li>\n<li><strong>ADD<\/strong> ICP: insurance industry support<\/li>\n<li><strong>MOD<\/strong> mark lead as lost at end of sales funnel<\/li>\n<li><strong>MOD<\/strong> api.device.show performance improvement<\/li>\n<li><strong>MOD<\/strong> positions queue to process just positions updates in the background faster to ensure live experience for users<\/li>\n<li><strong>MOD<\/strong> sales emails improvement<\/li>\n<li><strong>MOD<\/strong> remove throttling on device creation<\/li>\n<li><strong>MOD<\/strong> app stats as lazy loaded livewire component<\/li>\n<li><strong>MOD<\/strong> better lead qualification flow<\/li>\n<li><strong>MOD<\/strong> landing page: remove &quot;real-time&quot; wording<\/li>\n<li><strong>FIX<\/strong> redis bug on notification instantiation<\/li>\n<li><strong>FIX<\/strong> lead unsubscribe UI text contrast<\/li>\n<li><strong>ADD<\/strong> API for fuel tank evolution over time<\/li>\n<li><strong>ADD<\/strong> add device time series api\n<ul>\n<li><strong>ADD<\/strong> map poster image generation on landing<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<h2>2025-12<\/h2>\n<ul>\n<li><strong>MOD<\/strong> DB optimization for vehicle trips api<\/li>\n<li><strong>ADD<\/strong> api.device.index: filter by online, disable, driving status<\/li>\n<li><strong>ADD<\/strong> user can share vehicle access with other users in vehicle settings<\/li>\n<li><strong>MOD<\/strong> plan features to include short description to help users pick the right plan<\/li>\n<li><strong>MOD<\/strong> api.device.index: do not load relationships for slimer api response<\/li>\n<li><strong>MOD<\/strong> api.device.show: load all relationships<\/li>\n<li><strong>FIX<\/strong> fix presence channel for socket auth<\/li>\n<li><strong>ADD<\/strong> validation to prevent user from adding more vehicles pending on setup until done with the previous one<\/li>\n<li><strong>MOD<\/strong> rename device permission for assign\/unassign<\/li>\n<li><strong>ADD<\/strong> device.user crud permissions for granular control on device access<\/li>\n<li><strong>MOD<\/strong> yearly plan back to 16.67% discount instead of 50%<\/li>\n<li><strong>MOD<\/strong> bi-yearly plan back to 8.33% discount instead of 33.33%<\/li>\n<li><strong>ADD<\/strong> subscription api instead of device.subscription api: better design and use on mobile app<\/li>\n<li><strong>ADD<\/strong> redacted field for phone and unique. Only show when device permission allows<\/li>\n<li><strong>ADD<\/strong> vehicle settings: tracker inactive notification<\/li>\n<li><strong>MOD<\/strong> notify device users when device subscription is expiring or terminated because users might miss important notification<\/li>\n<\/ul>\n<h2>2025-11<\/h2>\n<ul>\n<li><strong>ADD<\/strong> tracker battery level on ui and api<\/li>\n<li><strong>ADD<\/strong> capture total more kpis: total ad spent, marketing efficiency ratio, cost per acquisition, average order value<\/li>\n<li><strong>ADD<\/strong> tracker total distance traveled and total engine hours<\/li>\n<li><strong>MOD<\/strong> how gps works on landing page and lead capture flow to better explain the value prop<\/li>\n<li><strong>ADD<\/strong> fuel tank as new device category<\/li>\n<li><strong>ADD<\/strong> vehicle events: filter vehicle event by types<\/li>\n<li><strong>ADD<\/strong> vehicle events: filter date range<\/li>\n<li><strong>ADD<\/strong> a team has drivers<\/li>\n<li><strong>ADD<\/strong> a driver can be assigned to vehicles<\/li>\n<li><strong>ADD<\/strong> a team has deliveries<\/li>\n<li><strong>ADD<\/strong> place autocomplete api from search<\/li>\n<li><strong>ADD<\/strong> team delivery model, resource and crud api<\/li>\n<li><strong>ADD<\/strong> team customer model, resource and crud api<\/li>\n<li><strong>ADD<\/strong> delivery for a team will create a record for a customer<\/li>\n<li><strong>ADD<\/strong> notification when customer subscription has been extended by staff<\/li>\n<li><strong>ADD<\/strong> lead automatically added to product update funnels when signing up<\/li>\n<\/ul>\n<h2>2025-10<\/h2>\n<ul>\n<li><strong>MOD<\/strong> recharge sim data as a separate job. reuse the same for when data expires but sub is still active (ie yearly)<\/li>\n<li><strong>ADD<\/strong> record transaction when sim data recharged<\/li>\n<li><strong>ADD<\/strong> full db backup weekly to s3<\/li>\n<li><strong>ADD<\/strong> getting started step by step guide for new users<\/li>\n<li><strong>MOD<\/strong> top up sim data for only what's necessary to reach next billing cycle<\/li>\n<li><strong>MOD<\/strong> &quot;start tracking&quot; cta is now redirecting to guided onboarding experience instead of product sales<\/li>\n<li><strong>ADD<\/strong> &quot;skip&quot; qualifying questions on lead qualification form for better experience<\/li>\n<li><strong>ADD<\/strong> add data expiration date for auto renewal of sim data<\/li>\n<li><strong>ADD<\/strong> success magic login will create matching lead to put in sales funnel at &quot;new&quot; stage<\/li>\n<li><strong>ADD<\/strong> translate new getting started guide to french from lead capture to checkout<\/li>\n<li><strong>FIX<\/strong> double encoding for user device permissions<\/li>\n<li><strong>ADD<\/strong> team has children and parent teams<\/li>\n<li><strong>ADD<\/strong> endpoint to retrieve all teams a user is part from<\/li>\n<li><strong>FIX<\/strong> sales funnel email queue<\/li>\n<li><strong>ADD<\/strong> team api crud<\/li>\n<li><strong>ADD<\/strong> team.user api crud<\/li>\n<li><strong>ADD<\/strong> team-permission.index api<\/li>\n<li><strong>ADD<\/strong> team.sub-team crud api<\/li>\n<li><strong>ADD<\/strong> total_engine_hours and total_distance for device json response<\/li>\n<li><strong>MOD<\/strong> do not send sales funnel email if lead already converted to customer or if lead already have access to device through access sharing<\/li>\n<\/ul>\n<h2>2025-09<\/h2>\n<ul>\n<li><strong>ADD<\/strong> lead, funnel and funnel stages for email marketing sequences<\/li>\n<li><strong>ADD<\/strong> Meta conversion api for ad optimization<\/li>\n<li><strong>MOD<\/strong> Sales funnel to use redis as connection for longed delayed jobs<\/li>\n<li><strong>ADD<\/strong> lead qualification form<\/li>\n<li><strong>ADD<\/strong> lead attribution with first party cookies for tracking source of lead<\/li>\n<li><strong>FIX<\/strong> subscription price for different duration amount bug<\/li>\n<li><strong>ADD<\/strong> monthly analytic capture of churn<\/li>\n<li><strong>ADD<\/strong> vehicle settings: activity report<\/li>\n<li><strong>ADD<\/strong> landing page: video embedding to thumbnail + click<\/li>\n<\/ul>\n<h2>2025-06<\/h2>\n<ul>\n<li><strong>ADD<\/strong> device transaction history for revenue and expense tracking\n<ul>\n<li>this is a first step towards a more complete accounting system for the business<\/li>\n<li>the goal is to be able to track revenue and expenses per device, per user, per month, etc<\/li>\n<\/ul>\n<\/li>\n<li><strong>ADD<\/strong> device settings for cashflow to enable accepting payment from third parties<\/li>\n<li><strong>ADD<\/strong> aggregates for device transactions\n<ul>\n<li>this will allow us to see the total revenue and expenses per device, per user, per month, etc but also same metrics per mileage<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<h2>2025-01<\/h2>\n<ul>\n<li><strong>ADD<\/strong> notification on hard turning of vehicle. Hard turning defined as a variation of course greater than 10 degree<\/li>\n<li><strong>ADD<\/strong> limit on active session for free trial plan users<\/li>\n<li><strong>ADD<\/strong> subscription command to check on subscription ending soon to notify customers. Paid plans will automatically downgrade to free plan and notify once more customers about that change.\n<ul>\n<li>the goal behind this is to only charge customers for what they need.<\/li>\n<li>however 3 days past due date, paid plan will switch to free plan allowing users to still benefit from app feature<\/li>\n<li>tracking should still work as long as airtime have been purchase<\/li>\n<\/ul>\n<\/li>\n<li><strong>ADD<\/strong> add MRR tracking over time\n<ul>\n<li>previous had just a stat card that capture the current MRR value<\/li>\n<li>now making a capture of it every day at midnight to see the progression over time<\/li>\n<li>For now, only capturing the MRR progression for upto a year not more<\/li>\n<\/ul>\n<\/li>\n<li><strong>MOD<\/strong> Hard acceleration\/braking\/turning have max window reduced from 60s to 30s. The data is not that fresh in a 60s window. The process was creating a significant amount of data that ended up not being needed at all<\/li>\n<li><strong>MOD<\/strong> Cap course angle range to 360.\n<ul>\n<li>some gps trackers were recording more than 360 angle value. For our data augmentation purposes I choose to to cap the value (ie 362 becomes 360) when computing angle acceleration<\/li>\n<\/ul>\n<\/li>\n<li><strong>ADD<\/strong> create\/delete transaction based on payment status<\/li>\n<li><strong>ADD<\/strong> Towing feature. User will be notified when vehicle being towed after being parked, meaning vehicle is moving but ignition is off. User can configure the minimum distance or speed. Yes speed also triggers the towing when vehicle engine is off but recorded a speed<\/li>\n<li><strong>ADD<\/strong> Power cut, and low battery events to bubble the information about gps tracker battery or power status.<\/li>\n<li><strong>ADD<\/strong> notification when trip start or ends.\n<ul>\n<li>TODO: expand on the why and how impactful it is for vehicle owners. Add few example use cases<\/li>\n<\/ul>\n<\/li>\n<li><strong>ADD<\/strong> subscription management: user can renew subscription in app<\/li>\n<li><strong>ADD<\/strong> subscription management: user can change plan (ie from trial to premium or platinum)<\/li>\n<li><strong>ADD<\/strong> endpoints for subscription plan list and support payment methods.\n<ul>\n<li>Helps control what is supported from the backend without the need for an app change<\/li>\n<\/ul>\n<\/li>\n<li><strong>ADD<\/strong> created_at field for users to know when their account was created.\n<ul>\n<li>Later we can use this information for many things like discount for long time users or on the onboarding flow<\/li>\n<\/ul>\n<\/li>\n<li><strong>ADD<\/strong> no more free use of traxelio. Going forward free plan is limited in time and will expire. Once free plan comes to an end, tracking will stop working until customer upgrades to a paid plan<\/li>\n<li><strong>ADD<\/strong> a new filter in devices list to filter out based on certain flags being on or not<\/li>\n<li><strong>ADD<\/strong> hard acceleration\/braking\/turning, low battery, power cut device setting apis<\/li>\n<li><strong>ADD<\/strong> device geo zone create and list apis<\/li>\n<li><strong>ADD<\/strong> dashboard: monthly active user over the last 90 days<\/li>\n<li><strong>ADD<\/strong> dashboard: api calls by user over the last 90 days<\/li>\n<li><strong>ADD<\/strong> geozone support for circle and polygon<\/li>\n<li><strong>ADD<\/strong> geozone created for device is automatically tied to auth user for later reuse<\/li>\n<li><strong>ADD<\/strong> capture metrics: MRR, All time revenue and profit over time. Display chart over time for last 90 days<\/li>\n<li><strong>ADD<\/strong> magic login for user to authenticate using email instead of password\n<ul>\n<li>no password management<\/li>\n<li>better security<\/li>\n<li>better UX<\/li>\n<li>users need email to have this feature<\/li>\n<li>users with password can still use their password to authenticate<\/li>\n<\/ul>\n<\/li>\n<li><strong>ADD<\/strong> fix speed limit notification ratelimit not working (cache key mismatch)<\/li>\n<li><strong>MOD<\/strong> skip position event job when event old than 5 minutes<\/li>\n<li><strong>MOD<\/strong> app retain state of command to simplify UI on remote shutdown and also enabling alarm for gps coban 403<\/li>\n<li><strong>ADD<\/strong> device state when command sent to propagate via web socket so that other users with shared access to gps, can see up to date state<\/li>\n<li><strong>ADD<\/strong> live chat pour augmenter la conversion.<\/li>\n<li><strong>ADD<\/strong> paiement via orange money<\/li>\n<li><strong>DEL<\/strong> remove free plan. Evaluation will be basic or premium plan that ends in a week<\/li>\n<li><strong>ADD<\/strong> vehicle vibration settings<\/li>\n<li><strong>ADD<\/strong> vehicle events screen (alerts included)<\/li>\n<li>user has a subscription on a vehicle<\/li>\n<li>subscription has one plan<\/li>\n<li>plan has many features<\/li>\n<li>feature on a subscription can be consummable<\/li>\n<li>subscription_feature_usages, limit per feature per vehicle tracked<\/li>\n<li><strong>ADD<\/strong> user settings api<\/li>\n<li><strong>ADD<\/strong> user subscription api with filter for expired ones<\/li>\n<li><strong>ADD<\/strong> event: description for a user friendly description of what the event or alarm means<\/li>\n<li><strong>ADD<\/strong> api for device creation<\/li>\n<li><strong>ADD<\/strong> currency support from user preferences<\/li>\n<li><strong>ADD<\/strong> api for gps model (products)<\/li>\n<li><strong>MOD<\/strong> payment method api: add filter by currency<\/li>\n<li><strong>ADD<\/strong> locale support from user preferences<\/li>\n<li><strong>MOD<\/strong> locale and currency from saved user preferences, fallback to user device<\/li>\n<li><strong>ADD<\/strong> inspection module<\/li>\n<li><strong>ADD<\/strong> api for user device document with filter on expiring<\/li>\n<li><\/li>\n<\/ul>\n<h2>2024-12<\/h2>\n<ul>\n<li><strong>MOD<\/strong> touch up blog page to look better.\n<ul>\n<li>Reused code from my personal blog post but migrated fully to tailwind classes<\/li>\n<li>Weekly changelog will be living there going forward<\/li>\n<\/ul>\n<\/li>\n<li><strong>REMOVE<\/strong> laravel mix - webpack<\/li>\n<li><strong>ADD<\/strong> vite as asset bundler<\/li>\n<li>Getting some sample from GPS supplier for research and development purposes\n<ul>\n<li>Freematics<\/li>\n<li>Solar powered GPS + magnetic GPS for container shipment tracking across the US from east to west coast and internationally later on<\/li>\n<li>The goal with this is to increase visibilily between port, fill the data gap when ship is in transit<\/li>\n<li>TODO: emphasize value added<\/li>\n<\/ul>\n<\/li>\n<li>Decision to remain a GPS tracking solution for automobile industry only.\n<ul>\n<li>specialization to become expert<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<h2>2024-11<\/h2>\n<ul>\n<li>Add online payment\n<ul>\n<li>use cos id and transaction id to determine success of a transaction later<\/li>\n<li>client paying for something created a payment<\/li>\n<li>payment has status: pending, completed, cancelled<\/li>\n<li>payment has product (service) and buyer (user)<\/li>\n<li>payment has method: cash, wave<\/li>\n<li>payment: id, amount, method, status, product_id, product_type, buyer_id, buyer_type<\/li>\n<li>wave webhook to update payment status to completed from 'client_reference'<\/li>\n<li>event for when payment is completed, websocket broadcast to web and mobile apps<\/li>\n<li>client initiate payment<\/li>\n<\/ul>\n<\/li>\n<li>geofence notification setting per device<\/li>\n<li>over speed notification setting per device<\/li>\n<li><strong>ADD<\/strong> product resource\n<ul>\n<li>users can now purchase GPS online. Choice have been limited to 3 products for sale.<\/li>\n<li>ecom (gps resale) is also a revenue stream for this project<\/li>\n<li>user provisioning as an online experience hoping to sell while I sleep<\/li>\n<li>the goal is to move towards that self service experience<\/li>\n<li>still need to do the fulfillment part<\/li>\n<\/ul>\n<\/li>\n<li><strong>ADD<\/strong> landing page: section for featured gps<\/li>\n<li><strong>ADD<\/strong> download page where we can redirect users from any channels. Auto redirect to appropriate app\/play store based on user device<\/li>\n<li><strong>ADD<\/strong> landing page flow: main CTA no longer leads to booking form but to 3 featured gps for sale. This works towards self service<\/li>\n<li><strong>ADD<\/strong> landing page multi language: french and english based on user locale<\/li>\n<li><strong>ADD<\/strong> landing page: add &quot;why choose us&quot; section to better express our mission<\/li>\n<li><strong>ADD<\/strong> Airtime status that checks daily when airtime on sim card needs to be renew.\n<ul>\n<li>Note that this one is different from the offline monitoring that runs every 15min. There is a bit of overlap with these too but mostly but having airtime monitoring separate allows us to send precise notification and suggest best action based on the events happening<\/li>\n<li>B2C SASS doesn't really work here because people are very conservative in spending a fixed fee<\/li>\n<li>B2B SASS on the other hand can work but then you have to deal with the whole corporate bullshit<\/li>\n<\/ul>\n<\/li>\n<li><strong>ADD<\/strong> product details page: user can now pay online for gps using Wave<\/li>\n<li>users can also pick the subscription plan they are going with (free trial, premium or platinum)\n<ul>\n<li>This sets the ground for self service with plug and play gps to come<\/li>\n<\/ul>\n<\/li>\n<li><strong>ADD<\/strong> users on free plan can buy airtime when it's about to expire or when the need comes<\/li>\n<li><strong>ADD<\/strong> Features pages to document features that known and the other that are lesser known\n<ul>\n<li>this should also boost SEO and discoverability when users are shopping around, this could make the deal fall in our favor.<\/li>\n<\/ul>\n<\/li>\n<li>Consistently posting one video a day to youtube shorts, instagram and facebook reel. Will start including LinkedIn<\/li>\n<li><strong>MOD<\/strong> Touch up activity report and scheduling for interested customer\n<ul>\n<li>add UI action to trigger for customers that don't know they need that yet. Having that button will come handy when we send the notification to customers few days after as part of their onboarding experience<\/li>\n<\/ul>\n<\/li>\n<li><strong>MOD<\/strong> acceleration creation used to a cron job running every 5min to determine whether or not a vehicle was overspeeding. Now this has moved to more instantly by computing it as it happens<\/li>\n<\/ul>\n<h2><a href=\"#2024-12-02\">2024-12-02<\/a><\/h2>\n<ul>\n<li>curfew status check relies on server date instead of mysql date. Because later we can always change application timezone away from UTC and curfew checks will still work\n<ul>\n<li>Avoid theft or unauthorized usage<\/li>\n<\/ul>\n<\/li>\n<li><strong>ADD<\/strong> api to retrieve, set and delete notification settings for: speed limit, curfew and geofences<\/li>\n<li><strong>MOD<\/strong> alarm vibration event from 5min cron to event base instantaneous notification<\/li>\n<li><strong>ADD<\/strong> radio team selection for device category. Was possible only when booking installation but now able to change it past that step in case of any error<\/li>\n<li><strong>ADD<\/strong> new reason: low battery. Determine possible reasons why GPS is offline from the events previously received. GPS can be sending 'low_battery', 'low_power', 'power_off', 'device_offline' events. When monitoring for GPS status we attempt to provide a reason why it is offline in order to recommend next best action: renew GPS data, check vehicle battery, upgrade to premium plan (please give us your money), etc<\/li>\n<li><strong>MOD<\/strong> Offline monitoring as default behavior for new gps added on the platform going forward.<\/li>\n<li><strong>ADD<\/strong> Autocomplete for admin users when a return user is adding a new gps to their fleet\n<ul>\n<li>Appointment setting for booking events from inventory management, google review request from the user to notification when device configuration is completed<\/li>\n<\/ul>\n<\/li>\n<li><strong>ADD<\/strong> Custom rate limiting function instead of built in to use exponential backoff for notification. Time between attempts iteration: 36min, 121min, 6h, 22h, 74h, 10days, 34days\n<ul>\n<li>This allow users to not be fire hosed notification to the point they are missing important information.<\/li>\n<li>This also allow us to save cost for this bootstrapped product<\/li>\n<\/ul>\n<\/li>\n<li><strong>ADD<\/strong> Blog section to provide value to customer.\n<ul>\n<li>This provides a way to rank higher on SEO and increase chance to be picked up on LLM (ie ChatGPT)<\/li>\n<\/ul>\n<\/li>\n<li><strong>FIX<\/strong> trip history last leg returning 0 as mileage<\/li>\n<li><strong>ADD<\/strong> new icon for device category bus which makes it 5 now with motorcycle, car, pickup and truck<\/li>\n<li><strong>ADD<\/strong> Sales process update:\n<ul>\n<li>add 30s video demo as part of initial conversation with client puts them at ease and increased significantly conversion.<\/li>\n<li>addressing price resistance by proposing 3 GPS at 3 different price points $40, $60 and $70<\/li>\n<\/ul>\n<\/li>\n<li><strong>ADD<\/strong> support for card as payment method<\/li>\n<li><strong>ADD<\/strong> log when user initiate checkout<\/li>\n<li><strong>ADD<\/strong> device has documents<\/li>\n<\/ul>\n<h2><a href=\"#2024-11-10\">2024-11-10<\/a><\/h2>\n<ul>\n<li>Fix device offline check logic<\/li>\n<li>Add read receipt on push notification<\/li>\n<li>Device offline logic to be computed from DB query rather that pulling every record (CPU vs DB)<\/li>\n<li>Curfew notification settings<\/li>\n<li>device offline: add reason as optional parameter for when offline reason is no airtime available<\/li>\n<li>health check: offline monitoring can be disabled per device<\/li>\n<li>add invoice notification for customer<\/li>\n<\/ul>\n<h2><a href=\"#2024-10-25\">2024-10-25<\/a><\/h2>\n<ul>\n<li>Expo push notification as a channel for notification<\/li>\n<\/ul>\n<h2><a href=\"#2024-08-22\">2024-08-22<\/a><\/h2>\n<ul>\n<li>smart tracking updates of devices are reflected on the UI as it happens<\/li>\n<\/ul>\n<h2><a href=\"#2024-08-09\">2024-08-09<\/a><\/h2>\n<ul>\n<li>Paid users will see their devices disabled when subscription expired. Once device disabled, it won't receive further tracking updates. Only way forward for users will be to either pay for the subscription to enable back tracking updates or downgrade to free plan<\/li>\n<\/ul>\n"