[{"data":1,"prerenderedAt":607},["ShallowReactive",2],{"navigation":3,"/blog/case-studies/retail-sap-quotation-app":51,"/blog/case-studies/retail-sap-quotation-app-surround":602},[4],{"title":5,"path":6,"stem":7,"children":8,"page":34},"Blog","/blog","blog",[9,13,35,39,43,47],{"title":10,"path":11,"stem":12},"Using the official exchange rate in Venezuela's e-commerce","/blog/bcv-scraper","blog/bcv-scraper",{"title":14,"path":15,"stem":16,"children":17,"page":34},"Case Studies","/blog/case-studies","blog/case-studies",[18,22,26,30],{"title":19,"path":20,"stem":21},"Rebuilding an e-commerce platform on NestJS + Next.js + Medusa in three months","/blog/case-studies/retail-ecommerce-rebuild","blog/case-studies/retail-ecommerce-rebuild",{"title":23,"path":24,"stem":25},"Stabilizing an inherited e-commerce platform on NestJS + Strapi","/blog/case-studies/retail-ecommerce-recovery","blog/case-studies/retail-ecommerce-recovery",{"title":27,"path":28,"stem":29},"A nationwide mobile sales-quotation app, integrated with SAP","/blog/case-studies/retail-sap-quotation-app","blog/case-studies/retail-sap-quotation-app",{"title":31,"path":32,"stem":33},"Shipping a high-scale PWA from an inherited e-commerce project","/blog/case-studies/supermarket-pwa-recovery","blog/case-studies/supermarket-pwa-recovery",false,{"title":36,"path":37,"stem":38},"From Nuxt to Hugo and Back Again","/blog/from-nuxt-to-hugo","blog/from-nuxt-to-hugo",{"title":40,"path":41,"stem":42},"The Intern is downloading movies again","/blog/intern-movie-torrent","blog/intern-movie-torrent",{"title":44,"path":45,"stem":46},"Japanese input in openSUSE Tumbleweed's KDE","/blog/japanese-input","blog/japanese-input",{"title":48,"path":49,"stem":50},"Jhey Pi: an official landing site for an aspiring musician","/blog/jp-landing","blog/jp-landing",{"id":52,"title":27,"author":53,"body":59,"category":578,"client":579,"date":580,"description":581,"extension":582,"featured":583,"image":584,"meta":585,"minRead":586,"navigation":583,"ogImage":584,"outcome_headline":587,"path":28,"repoUrl":586,"role":588,"seo":589,"stack":590,"stem":29,"tags":596,"team_size":571,"type":109,"year":600,"__hash__":601},"blog/blog/case-studies/retail-sap-quotation-app.md",{"name":54,"username":55,"to":56,"avatar":57},"Iván Álvarez","ivanovertime","https://github.com/ivanovertime",{"src":58,"alt":54},"/avatar.jpg",{"type":60,"value":61,"toc":561},"minimark",[62,67,85,89,93,104,111,115,183,186,190,195,198,238,241,245,248,284,287,291,294,330,333,337,340,376,379,383,386,422,426,429,459,462,466,469,543,546,549],[63,64,66],"h2",{"id":65},"tldr","TL;DR",[68,69,70,74,82],"ul",{},[71,72,73],"li",{},"Greenfield brief: replace an inconsistent, offline-tolerant quotation flow with a mobile-first app for distributed sales teams.",[71,75,76,77,81],{},"I led the architecture and led a team of 3 to ship a ",[78,79,80],"strong",{},"mobile-first quotation app"," on Laravel + Filament, with SAP as the source of truth.",[71,83,84],{},"The app rolled out to sales staff across the country and gave the business a real-time view of quotation activity alongside the existing SAP flow.",[63,86,88],{"id":87},"context","Context",[90,91,92],"p",{},"The client had sales teams across the country handling business-to-business and high-value retail quotations. The shape of the brief is one anyone who has digitized a sales process will recognise:",[68,94,95,98,101],{},[71,96,97],{},"A salesperson surface that needed to work on a phone, in a store, on a variable connection.",[71,99,100],{},"A quotation flow whose system of record was SAP, with everything else expected to converge on it.",[71,102,103],{},"A business that wanted a live picture of quotation activity rather than only what eventually landed downstream.",[90,105,106,107,110],{},"The mandate was different from the rescues: this one was ",[78,108,109],{},"greenfield",", but with a hard integration constraint. SAP was the system of record and was not going to move. Anything new had to plug into it without becoming another fragile bridge.",[63,112,114],{"id":113},"constraints","Constraints",[116,117,118,131],"table",{},[119,120,121],"thead",{},[122,123,124,128],"tr",{},[125,126,127],"th",{},"Constraint",[125,129,130],{},"Reality",[132,133,134,143,151,159,167,175],"tbody",{},[122,135,136,140],{},[137,138,139],"td",{},"Team size",[137,141,142],{},"3 engineers, with me as architect and tech lead",[122,144,145,148],{},[137,146,147],{},"Form factor",[137,149,150],{},"Mobile-first — sales staff work on phones in stores and on the road",[122,152,153,156],{},[137,154,155],{},"Source of truth",[137,157,158],{},"SAP for catalog, pricing, customers, and quotation persistence",[122,160,161,164],{},[137,162,163],{},"Connectivity",[137,165,166],{},"Sales sites with variable network quality — the app had to degrade gracefully",[122,168,169,172],{},[137,170,171],{},"Rollout",[137,173,174],{},"Nationwide, across multiple sales teams with different habits",[122,176,177,180],{},[137,178,179],{},"Adjacent systems",[137,181,182],{},"The same SAP integration patterns I'd built for the e-commerce rescue could be reused",[90,184,185],{},"The non-negotiable: the salesperson on the floor must always be able to produce a credible quote, even on a flaky connection.",[63,187,189],{"id":188},"decisions","Decisions",[191,192,194],"h3",{"id":193},"decision-1-laravel-filament-for-speed-to-rollout-not-for-novelty","Decision 1 — Laravel + Filament for speed-to-rollout, not for novelty",[90,196,197],{},"The temptation in 2025 was to reach for a fashionable stack. The right answer for a small team and a nationwide deadline was the one we could ship.",[116,199,200,213],{},[119,201,202],{},[122,203,204,207,210],{},[125,205,206],{},"Option",[125,208,209],{},"Trade-off",[125,211,212],{},"Chose",[132,214,215,225],{},[122,216,217,220,223],{},[137,218,219],{},"A bespoke SPA + custom admin",[137,221,222],{},"Months of UI work before the back-office could see anything",[137,224],{},[122,226,227,230,233],{},[137,228,229],{},"Laravel + Filament for the back-office; a thin mobile-first PWA for the salesperson surface",[137,231,232],{},"Filament gives a production-grade admin in days; Laravel handles the SAP integration cleanly; the team already knew the stack",[137,234,235],{},[236,237],"decision-check",{},[90,239,240],{},"Boring on purpose. The novelty budget went into the SAP integration, not the framework choice.",[191,242,244],{"id":243},"decision-2-mobile-first-pwa-not-a-native-app","Decision 2 — Mobile-first PWA, not a native app",[90,246,247],{},"Same reasoning as earlier PWA work, applied at higher stakes.",[116,249,250,260],{},[119,251,252],{},[122,253,254,256,258],{},[125,255,206],{},[125,257,209],{},[125,259,212],{},[132,261,262,272],{},[122,263,264,267,270],{},[137,265,266],{},"Native iOS + Android",[137,268,269],{},"App-store gauntlet on every release; longer build cycles; device fragmentation",[137,271],{},[122,273,274,277,280],{},[137,275,276],{},"Mobile-first PWA, installable from a link",[137,278,279],{},"Same codebase as the back-office; instant updates; deploys decoupled from app stores",[137,281,282],{},[236,283],{},[90,285,286],{},"For a tool used by employees rather than customers, a PWA was the obvious right answer — fewer moving parts, faster iteration, no rollout choreography.",[191,288,290],{"id":289},"decision-3-treat-sap-as-a-contract-not-as-a-runtime-dependency","Decision 3 — Treat SAP as a contract, not as a runtime dependency",[90,292,293],{},"SAP was the system of record. It wasn't, however, fast or always available. If the app called SAP synchronously on every action, the salesperson on the floor would feel every SAP hiccup.",[116,295,296,306],{},[119,297,298],{},[122,299,300,302,304],{},[125,301,206],{},[125,303,209],{},[125,305,212],{},[132,307,308,318],{},[122,309,310,313,316],{},[137,311,312],{},"Call SAP synchronously for every read and write",[137,314,315],{},"Simplest mental model; the salesperson lives at SAP's mercy",[137,317],{},[122,319,320,323,326],{},[137,321,322],{},"Project SAP catalog and pricing into the app's own store; queue writes back to SAP",[137,324,325],{},"More moving parts; the salesperson always sees a usable catalog and the system tolerates SAP outages",[137,327,328],{},[236,329],{},[90,331,332],{},"This is the pattern I carried over from the e-commerce rescue: a single integration module with a typed interface, observable, retry-aware, and replaceable. The app reads from a local projection and writes through the module — SAP is the contract, not the runtime.",[191,334,336],{"id":335},"decision-4-design-for-variable-connectivity-from-day-one","Decision 4 — Design for variable connectivity from day one",[90,338,339],{},"Sales sites don't have a uniform network story. Treating offline as an exception meant it would never be tested; treating it as a default meant it just worked.",[116,341,342,352],{},[119,343,344],{},[122,345,346,348,350],{},[125,347,206],{},[125,349,209],{},[125,351,212],{},[132,353,354,364],{},[122,355,356,359,362],{},[137,357,358],{},"Assume the network and add offline later",[137,360,361],{},"Offline becomes a perpetually-postponed Phase 2",[137,363],{},[122,365,366,369,372],{},[137,367,368],{},"Cache the catalog locally, queue quotes, sync when online",[137,370,371],{},"Up-front design cost; the app behaves the same in the warehouse and on a highway",[137,373,374],{},[236,375],{},[90,377,378],{},"The business outcome of this decision was simple: a salesperson on a bad connection still closes the quote.",[191,380,382],{"id":381},"decision-5-filament-as-the-back-office-not-as-the-strategy","Decision 5 — Filament as the back-office, not as the strategy",[90,384,385],{},"Filament made the admin surface cheap to build. That meant we could spend the saved time on the SAP integration and the mobile experience — not on yet another bespoke admin.",[116,387,388,398],{},[119,389,390],{},[122,391,392,394,396],{},[125,393,206],{},[125,395,209],{},[125,397,212],{},[132,399,400,410],{},[122,401,402,405,408],{},[137,403,404],{},"Build a custom admin from scratch",[137,406,407],{},"Fully bespoke, fully ours, fully unfinished",[137,409],{},[122,411,412,415,418],{},[137,413,414],{},"Lean on Filament for the back-office and put the engineering time into integration and mobile UX",[137,416,417],{},"The interesting work goes where the leverage is",[137,419,420],{},[236,421],{},[63,423,425],{"id":424},"outcome","Outcome",[90,427,428],{},"By rollout:",[68,430,431,438,445,452],{},[71,432,433,434,437],{},"The app was ",[78,435,436],{},"deployed across multiple regions",", covering multiple sales teams.",[71,439,440,441,444],{},"Quotes were ",[78,442,443],{},"consistent with SAP catalog and pricing"," at the moment of generation.",[71,446,447,448,451],{},"The business gained a ",[78,449,450],{},"real-time view of quotation activity"," alongside the SAP-of-record view.",[71,453,454,455,458],{},"The SAP integration module became a ",[78,456,457],{},"reusable pattern"," across the other platforms my team worked on in the same engagement.",[90,460,461],{},"This was the build where the integration-module pattern proved generalizable: write the SAP boundary once, reuse it across products.",[63,463,465],{"id":464},"what-id-do-on-gcp-today","What I'd do on GCP today",[90,467,468],{},"Same brief today, same constraints, same SAP. The architecture instincts wouldn't change; the platform underneath would.",[68,470,471,481,491,509,523,533],{},[71,472,473,476,477,480],{},[78,474,475],{},"Back-office runtime:"," containerize the Laravel app for ",[78,478,479],{},"Cloud Run",". Same artifact, no host-management work.",[71,482,483,486,487,490],{},[78,484,485],{},"SAP integration:"," keep the single-module pattern. Move the writes onto ",[78,488,489],{},"Pub/Sub"," so the app never blocks on SAP, and the integration module becomes a Pub/Sub consumer with retries and a dead-letter topic.",[71,492,493,496,497,500,501,504,505,508],{},[78,494,495],{},"Catalog + pricing projection:"," sync from SAP into ",[78,498,499],{},"BigQuery"," (analytics) and ",[78,502,503],{},"Firestore"," or ",[78,506,507],{},"Cloud SQL"," (hot reads). The mobile app reads from the projection; SAP stays the system of record.",[71,510,511,514,515,518,519,522],{},[78,512,513],{},"Mobile surface:"," the same PWA, hosted on ",[78,516,517],{},"Firebase Hosting"," with offline caching via service workers. Optionally wrapped with Capacitor and shipped through ",[78,520,521],{},"Firebase App Distribution"," for staged rollouts.",[71,524,525,528,529,532],{},[78,526,527],{},"Real-time demand view:"," stream quote events into BigQuery and surface them in ",[78,530,531],{},"Looker Studio"," for the business — the data surface that was a side-effect on-prem becomes a first-class deliverable.",[71,534,535,538,539,542],{},[78,536,537],{},"Delivery:"," ",[78,540,541],{},"GitHub Actions → Cloud Run + Firebase",", preview environment per PR. The same predictability we earned manually, but for free.",[90,544,545],{},"The shape of the work is the same as the rescues — a boring framework, a single integration boundary, an offline-first surface, and observability from day one — but the platform underneath does most of the heavy lifting.",[547,548],"hr",{},[90,550,551],{},[552,553,554,555,560],"em",{},"If you have a SAP-coupled process that still depends on manual coordination, I'd be glad to compare notes on what a small focused build could look like. ",[556,557,559],"a",{"href":558},"/contact","Reach out here",".",{"title":562,"searchDepth":563,"depth":563,"links":564},"",2,[565,566,567,568,576,577],{"id":65,"depth":563,"text":66},{"id":87,"depth":563,"text":88},{"id":113,"depth":563,"text":114},{"id":188,"depth":563,"text":189,"children":569},[570,572,573,574,575],{"id":193,"depth":571,"text":194},3,{"id":243,"depth":571,"text":244},{"id":289,"depth":571,"text":290},{"id":335,"depth":571,"text":336},{"id":381,"depth":571,"text":382},{"id":424,"depth":563,"text":425},{"id":464,"depth":563,"text":465},"case-study"," Retail Client (LatAm)","2026-05-15T00:00:00.000Z","Architecting and shipping a mobile-first sales-quotation application on Laravel + Filament, integrated with SAP as the source of truth for catalog and pricing, and rolled out to sales teams across the country.\n","md",true,"/blog/case-studies/retail-sap-quotation-app/featuredImage.jpg",{},null,"Replaced a paper-and-spreadsheet quotation flow with a mobile-first app rolled out nationwide","Architect & Tech Lead",{"title":27,"description":581},[591,592,593,594,595],"Laravel","Filament","SAP","Mobile-first PWA","Docker",[597,598,599],"e-commerce","integrations","sap",2025,"arSaKBqkrUFQlAEq2_sye7N9L_clVsrOcTvbfKRSZgc",[603,605],{"title":23,"path":24,"stem":25,"description":604,"children":-1},"A recovery-shaped engagement on an inherited NestJS + Strapi storefront: stabilize the runtime, contain the SAP integration behind a single boundary, containerize the deploy path, and keep Strapi to editorial content. This is the recovery half of a paired engagement; the rebuild that followed is its own story.\n",{"title":31,"path":32,"stem":33,"description":606,"children":-1},"How a stalled Node.js + Angular e-commerce project was diagnosed, recovered, and shipped end-to-end as a PWA and mobile apps for a high-volume customer base — with a Docker registry and CI/CD pipeline that made continuous delivery possible for the team that came after.\n",1779739958195]