<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Chris Lloyd Fallaria</title>
    <description>The latest articles on Forem by Chris Lloyd Fallaria (@chrislfallaria).</description>
    <link>https://forem.com/chrislfallaria</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3908488%2Ff1a01733-eafb-43fb-b97c-767dc1ecddb9.png</url>
      <title>Forem: Chris Lloyd Fallaria</title>
      <link>https://forem.com/chrislfallaria</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/chrislfallaria"/>
    <language>en</language>
    <item>
      <title>Why I Stopped Using Raw SQL Date Functions and Switched to Carbon in Laravel</title>
      <dc:creator>Chris Lloyd Fallaria</dc:creator>
      <pubDate>Tue, 05 May 2026 05:02:05 +0000</pubDate>
      <link>https://forem.com/chrislfallaria/why-i-stopped-using-raw-sql-date-functions-and-switched-to-carbon-in-laravel-2145</link>
      <guid>https://forem.com/chrislfallaria/why-i-stopped-using-raw-sql-date-functions-and-switched-to-carbon-in-laravel-2145</guid>
      <description>&lt;h2&gt;
  
  
  It Started With a Bug
&lt;/h2&gt;

&lt;p&gt;When I was building VMMS — a voucher management system &lt;br&gt;
for government offices — everything worked fine locally.&lt;/p&gt;

&lt;p&gt;MySQL. Clean queries. Fast results.&lt;/p&gt;

&lt;p&gt;Then I deployed to a server running MariaDB.&lt;/p&gt;

&lt;p&gt;Half my charts broke.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I had written date queries like this all over the codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This breaks on MariaDB&lt;/span&gt;
&lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'voucher_transactions'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;selectRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MONTHNAME(created_at) as month, COUNT(*) as total'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;groupByRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MONTH(created_at), MONTHNAME(created_at)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MONTH()&lt;/code&gt; and &lt;code&gt;MONTHNAME()&lt;/code&gt; are MySQL functions. They work &lt;br&gt;
fine on MySQL but behave differently on MariaDB — especially &lt;br&gt;
when combined with &lt;code&gt;GROUP BY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The result? Months showing up in the wrong order. &lt;br&gt;
Duplicate entries. Missing data.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why This Happens
&lt;/h2&gt;

&lt;p&gt;MySQL and MariaDB have diverged over the years. They share &lt;br&gt;
a lot of syntax but handle certain functions differently — &lt;br&gt;
especially around date grouping and ordering.&lt;/p&gt;

&lt;p&gt;The safe rule is: &lt;strong&gt;if you want your app to work on both, &lt;br&gt;
don't rely on database-specific date functions.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The Fix — Pull the Data and Use Carbon
&lt;/h2&gt;

&lt;p&gt;Instead of doing date calculations in SQL, I pulled the &lt;br&gt;
raw data and used Carbon to handle everything in PHP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DB-agnostic approach&lt;/span&gt;
&lt;span class="nv"&gt;$rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VoucherTransaction&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereYear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$year&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'deleted_at'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nv"&gt;$byMonth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rows&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'n'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$byMonth&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$byMonth&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'accomplished'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'rejected'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'accomplished'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$byMonth&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'accomplished'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'rejected'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="nv"&gt;$byMonth&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'rejected'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;ksort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$byMonth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then to get the month name I used Carbon instead of &lt;br&gt;
MONTHNAME():&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$monthName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Carbon&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'F'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Returns "January", "February", etc.&lt;/span&gt;
&lt;span class="c1"&gt;// Works the same on MySQL and MariaDB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Building the Full Year Chart
&lt;/h2&gt;

&lt;p&gt;For charts that need all 12 months — even empty ones — &lt;br&gt;
I use &lt;code&gt;range(1, 12)&lt;/code&gt; and fill in zeros for months with &lt;br&gt;
no data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$byMonth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$year&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$byMonth&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'accomplished'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'rejected'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'month'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Carbon&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'F'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'accomplished'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'accomplished'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'rejected'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'rejected'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This guarantees all 12 months always appear in the chart &lt;br&gt;
— even if there's no data for some months. Clean and &lt;br&gt;
predictable.&lt;/p&gt;


&lt;h2&gt;
  
  
  What About Performance?
&lt;/h2&gt;

&lt;p&gt;You might be thinking — isn't pulling all rows and &lt;br&gt;
processing in PHP slower than doing it in SQL?&lt;/p&gt;

&lt;p&gt;For most applications — yes, SQL aggregation is faster.&lt;/p&gt;

&lt;p&gt;But in practice for this use case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The data is filtered by user and year first&lt;/li&gt;
&lt;li&gt;The result set is small (max 365 rows per year per user)&lt;/li&gt;
&lt;li&gt;The PHP processing is negligible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're dealing with millions of rows this approach &lt;br&gt;
needs rethinking. But for typical business applications &lt;br&gt;
it's fast enough and the portability is worth it.&lt;/p&gt;


&lt;h2&gt;
  
  
  Other Carbon Tricks I Use in VMMS
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Comparing dates without time:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Use DATE() in SQL to avoid time comparison issues&lt;/span&gt;
&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DATE(deadline) &amp;gt;= ?'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$now&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toDateString&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;
&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DATE(deadline) &amp;lt;= ?'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$now&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toDateString&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Calculating days left:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Carbon&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;startOfDay&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$daysLeft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$now&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;startOfDay&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;diffInDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$deadline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;false&lt;/code&gt; parameter makes &lt;code&gt;diffInDays&lt;/code&gt; return negative &lt;br&gt;
numbers for past dates — useful for overdue detection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Calculating processing time:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$minutes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Carbon&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;process_initiate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;diffInMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Carbon&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;process_accomplished&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Lesson
&lt;/h2&gt;

&lt;p&gt;Don't assume your local MySQL behavior will match &lt;br&gt;
production. If your app might run on MariaDB, PostgreSQL, &lt;br&gt;
or any other database — keep your date logic in PHP &lt;br&gt;
with Carbon and use only standard SQL for filtering.&lt;/p&gt;

&lt;p&gt;It's a small habit that saves a lot of debugging time.&lt;/p&gt;




&lt;h2&gt;
  
  
  About VMMS
&lt;/h2&gt;

&lt;p&gt;This and many other lessons came from building VMMS — &lt;br&gt;
a complete voucher management system for government &lt;br&gt;
offices, companies, and educational institutions.&lt;/p&gt;

&lt;p&gt;🔴 Live demo: &lt;a href="https://vmms-app-production.up.railway.app/login" rel="noopener noreferrer"&gt;https://vmms-app-production.up.railway.app/login&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Available on Gumroad:&lt;br&gt;
👉 &lt;a href="https://getvmms.gumroad.com/l/zeroqz" rel="noopener noreferrer"&gt;https://getvmms.gumroad.com/l/zeroqz&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer any questions in the comments! 🚀&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>webdev</category>
      <category>php</category>
      <category>mysql</category>
    </item>
    <item>
      <title>How I Built a Multi-Department Workflow Routing System in Laravel with Inertia.js</title>
      <dc:creator>Chris Lloyd Fallaria</dc:creator>
      <pubDate>Sun, 03 May 2026 13:54:47 +0000</pubDate>
      <link>https://forem.com/chrislfallaria/how-i-built-a-multi-department-workflow-routing-system-in-laravel-with-inertiajs-4gm9</link>
      <guid>https://forem.com/chrislfallaria/how-i-built-a-multi-department-workflow-routing-system-in-laravel-with-inertiajs-4gm9</guid>
      <description>&lt;h2&gt;
  
  
  The Problem I Was Trying to Solve
&lt;/h2&gt;

&lt;p&gt;When I started building VMMS — a voucher management system &lt;br&gt;
for government offices — one of the hardest parts wasn't &lt;br&gt;
the UI or the database schema.&lt;/p&gt;

&lt;p&gt;It was the workflow routing.&lt;/p&gt;

&lt;p&gt;A voucher request doesn't just go to one office. It goes &lt;br&gt;
to multiple departments in a specific order — each one &lt;br&gt;
needs to process it before passing it to the next.&lt;/p&gt;

&lt;p&gt;And at any point, a department can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete their step and pass it forward&lt;/li&gt;
&lt;li&gt;Reject the entire request&lt;/li&gt;
&lt;li&gt;Flag it for missing documents and pause processing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I needed a system that could handle all of that cleanly.&lt;/p&gt;


&lt;h2&gt;
  
  
  How I Modeled It
&lt;/h2&gt;

&lt;p&gt;Every voucher type has a configurable processing flow &lt;br&gt;
stored in a &lt;code&gt;transaction_flows&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'transaction_flows'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'voucher_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cascadeOnDelete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'department'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'order_number'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a client submits a request, the system reads the &lt;br&gt;
flow for that voucher type and routes it to the first &lt;br&gt;
department automatically.&lt;/p&gt;


&lt;h2&gt;
  
  
  Tracking Progress with Audit Trails
&lt;/h2&gt;

&lt;p&gt;Every time a department processes a request, an audit &lt;br&gt;
trail record is created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'audit_trails'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'transaction_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cascadeOnDelete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'processing_offices'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'process_initiate'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'process_accomplished'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'deadline'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;process_initiate&lt;/code&gt; is set when a department starts &lt;br&gt;
processing. &lt;code&gt;process_accomplished&lt;/code&gt; is set when they &lt;br&gt;
finish. If only &lt;code&gt;process_initiate&lt;/code&gt; is set — that's &lt;br&gt;
the currently active department.&lt;/p&gt;


&lt;h2&gt;
  
  
  Finding the Current Department
&lt;/h2&gt;

&lt;p&gt;This was trickier than I expected. Here's the logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$activeAudit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$auditTrails&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;process_initiate&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;process_accomplished&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$activeAudit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$activeAudit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;processing_offices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// If no active audit, find the next step&lt;/span&gt;
&lt;span class="nv"&gt;$doneOrders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$auditTrails&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;process_accomplished&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$flow&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;firstWhere&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'department'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;processing_offices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;order_number&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$lastDone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$doneOrders&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$nextStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$flow&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;firstWhere&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'order_number'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$lastDone&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$nextStep&lt;/span&gt;&lt;span class="o"&gt;?-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'Completed'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tricky part was handling edge cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What if a department is skipped?&lt;/li&gt;
&lt;li&gt;What if a request is returned for missing documents?&lt;/li&gt;
&lt;li&gt;What if the last department just finished?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these needed its own handling.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pipeline Stepper on the Frontend
&lt;/h2&gt;

&lt;p&gt;On the Vue 3 side, I built a stepper component that &lt;br&gt;
shows each department as a step — green for done, &lt;br&gt;
pulsing for active, grey for pending.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stepStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stepIndex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audit_trails&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accomplishedCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;process_accomplished&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stepIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;accomplishedCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stepIndex&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;accomplishedCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple logic but it took a few rewrites to handle &lt;br&gt;
all the edge cases correctly.&lt;/p&gt;


&lt;h2&gt;
  
  
  Missing Documents — Pausing the Flow
&lt;/h2&gt;

&lt;p&gt;When a staff member flags a request for missing &lt;br&gt;
documents, the flow pauses. The client gets notified &lt;br&gt;
and needs to upload the corrected files before &lt;br&gt;
processing can continue.&lt;/p&gt;

&lt;p&gt;I handled this through a separate &lt;code&gt;requirement_validations&lt;/code&gt; &lt;br&gt;
table that tracks which transactions are waiting for &lt;br&gt;
documents from which user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$missingDocIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RequirementValidation&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'receiver_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'resume_transaction'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;pluck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'transaction_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;resume_transaction&lt;/code&gt; is filled — the flow resumes &lt;br&gt;
and the request goes back to the staff for continued &lt;br&gt;
processing.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Model the flow separately from the transaction&lt;/strong&gt;&lt;br&gt;
Keeping &lt;code&gt;transaction_flows&lt;/code&gt; separate from &lt;br&gt;
&lt;code&gt;voucher_transactions&lt;/code&gt; made it easy to change &lt;br&gt;
the processing order for a voucher type without &lt;br&gt;
affecting existing requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Audit trails are your best friend&lt;/strong&gt;&lt;br&gt;
Storing every processing event with timestamps &lt;br&gt;
made it easy to calculate processing times, &lt;br&gt;
track performance, and build analytics on top.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Edge cases will get you&lt;/strong&gt;&lt;br&gt;
The "happy path" was easy. Missing documents, &lt;br&gt;
skipped departments, and rejected requests &lt;br&gt;
all needed separate handling. Budget time for this.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;The pipeline tracker is now one of the most useful &lt;br&gt;
features in VMMS. Clients can see exactly where &lt;br&gt;
their request is in real time — no more following &lt;br&gt;
up in person.&lt;/p&gt;

&lt;p&gt;If you want to see it in action:&lt;/p&gt;

&lt;p&gt;🔴 Live demo: &lt;a href="https://vmms-app-production.up.railway.app/login" rel="noopener noreferrer"&gt;https://vmms-app-production.up.railway.app/login&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;VMMS is available on Gumroad if you want to use it &lt;br&gt;
or build on top of it:&lt;br&gt;
👉 &lt;a href="https://getvmms.gumroad.com/l/zeroqz" rel="noopener noreferrer"&gt;https://getvmms.gumroad.com/l/zeroqz&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer any questions in the comments!&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
      <category>inertia</category>
    </item>
    <item>
      <title>How I Built a Production-Ready Voucher Management System with Laravel 12 and Vue 3</title>
      <dc:creator>Chris Lloyd Fallaria</dc:creator>
      <pubDate>Sat, 02 May 2026 06:14:06 +0000</pubDate>
      <link>https://forem.com/chrislfallaria/how-i-built-a-production-ready-voucher-management-system-with-laravel-12-and-vue-3-3dl0</link>
      <guid>https://forem.com/chrislfallaria/how-i-built-a-production-ready-voucher-management-system-with-laravel-12-and-vue-3-3dl0</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I'm a CS student in the Philippines. During my time observing how &lt;br&gt;
offices work, I noticed something that kept bothering me.&lt;/p&gt;

&lt;p&gt;Voucher processing is still done manually almost everywhere. &lt;br&gt;
Someone fills out a paper form, hands it to the next office, &lt;br&gt;
that office stamps it and passes it again, and so on — until &lt;br&gt;
it either gets done or gets lost on someone's desk.&lt;/p&gt;

&lt;p&gt;No tracking. No visibility. The client has no idea where their &lt;br&gt;
request is. The admin has no data. Staff have no accountability.&lt;/p&gt;

&lt;p&gt;I kept thinking — this is exactly the kind of problem software &lt;br&gt;
should solve. So I spent the last few months building it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;VMMS stands for Voucher Management &amp;amp; Monitoring System.&lt;/p&gt;

&lt;p&gt;It handles the entire voucher request process — from the moment &lt;br&gt;
a client submits a request, all the way through each department &lt;br&gt;
that needs to process it, until it's finally released.&lt;/p&gt;

&lt;p&gt;No paper. No manual routing. No "what's the status of my request?"&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;I went with what I know best:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Laravel 12&lt;/strong&gt; for the backend — routing, queues, and email jobs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vue 3 (Composition API)&lt;/strong&gt; for the frontend — reactive and clean&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inertia.js v2&lt;/strong&gt; — so I don't have to build a separate API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS + Vuetify 3&lt;/strong&gt; — for UI components and styling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL 8.0&lt;/strong&gt; — for the relational data and complex queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend&lt;/strong&gt; — for transactional emails&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What It Can Do
&lt;/h2&gt;

&lt;p&gt;There are three separate panels depending on your role:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Admin&lt;/strong&gt;&lt;br&gt;
You get full control — manage users, voucher types, and see &lt;br&gt;
everything through an analytics dashboard. There's a staff &lt;br&gt;
performance leaderboard, rejection rate charts, audit logs, &lt;br&gt;
and even custom branding options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Staff&lt;/strong&gt;&lt;br&gt;
Each department gets their own queue of requests to process. &lt;br&gt;
You can complete, reject, or flag a request for missing documents. &lt;br&gt;
There's also a personal performance dashboard showing your stats &lt;br&gt;
and ratings from clients.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client&lt;/strong&gt;&lt;br&gt;
You submit requests online, track which department is currently &lt;br&gt;
processing your voucher in real time, upload corrected documents &lt;br&gt;
if something gets flagged, and rate the staff after it's done.&lt;/p&gt;

&lt;p&gt;Some other things worth mentioning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deadlines are holiday-aware — staff don't get penalized for 
non-working days&lt;/li&gt;
&lt;li&gt;Every action is logged in an audit trail&lt;/li&gt;
&lt;li&gt;PDF and Excel exports on all reports&lt;/li&gt;
&lt;li&gt;Automated email notifications at every stage&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Parts That Were Actually Hard
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Date queries across MySQL and MariaDB&lt;/strong&gt;&lt;br&gt;
I originally wrote date queries using MySQL-specific functions &lt;br&gt;
like MONTH() and MONTHNAME(). Broke on MariaDB. Had to rewrite &lt;br&gt;
everything to use Carbon instead — which honestly ended up &lt;br&gt;
being cleaner anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pipeline tracker&lt;/strong&gt;&lt;br&gt;
Getting the pipeline stepper to accurately show which department &lt;br&gt;
is currently processing a request was trickier than I expected. &lt;br&gt;
Especially edge cases — what if a department is skipped? What if &lt;br&gt;
a request gets returned for missing documents mid-flow? Took a &lt;br&gt;
few rewrites to get right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Holiday-aware deadlines&lt;/strong&gt;&lt;br&gt;
I built a DateHelpers service that calculates business days &lt;br&gt;
while excluding weekends and holidays. Sounds simple, took way &lt;br&gt;
longer than expected. Really happy with how it turned out though.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;🔴 Live demo: &lt;a href="https://vmms-app-production.up.railway.app/login" rel="noopener noreferrer"&gt;https://vmms-app-production.up.railway.app/login&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Credentials if you want to explore:&lt;/p&gt;

&lt;p&gt;Admin: &lt;a href="mailto:admin@vmms.demo"&gt;admin@vmms.demo&lt;/a&gt; | Password: Admin@Demo123!&lt;br&gt;
Staff:  &lt;a href="mailto:staff.infra@vmms.demo"&gt;staff.infra@vmms.demo&lt;/a&gt; | &lt;a href="mailto:staff.budget@vmms.demo"&gt;staff.budget@vmms.demo&lt;/a&gt; | &lt;a href="mailto:staff.procurement@vmms.demo"&gt;staff.procurement@vmms.demo&lt;/a&gt; | &lt;a href="mailto:staff.supply@vmms.demo"&gt;staff.supply@vmms.demo&lt;/a&gt; | &lt;a href="mailto:staff.executive@vmms.demo"&gt;staff.executive@vmms.demo&lt;/a&gt; | Password: Staff@Demo123!&lt;br&gt;
Client: &lt;a href="mailto:client@vmms.demo"&gt;client@vmms.demo&lt;/a&gt; | Password: Client@Demo123!&lt;/p&gt;




&lt;h2&gt;
  
  
  It's Available on Gumroad
&lt;/h2&gt;

&lt;p&gt;I'm selling VMMS in two versions:&lt;/p&gt;

&lt;p&gt;🔒 &lt;strong&gt;Obfuscated / Protected — $99&lt;/strong&gt;&lt;br&gt;
Deploy it as-is. PHP backend is protected, Vue frontend and &lt;br&gt;
config files are fully readable. Good for offices and &lt;br&gt;
institutions that just want to run it.&lt;/p&gt;

&lt;p&gt;👨‍💻 &lt;strong&gt;Full Source Code — $300&lt;/strong&gt;&lt;br&gt;
Every file is readable and editable. Good for developers who &lt;br&gt;
want to customize it for a specific client or use case.&lt;/p&gt;

&lt;p&gt;Both versions come with documentation, a quick start guide, &lt;br&gt;
and deployment configs for Nginx, Apache, and Railway.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://getvmms.gumroad.com/l/zeroqz" rel="noopener noreferrer"&gt;https://getvmms.gumroad.com/l/zeroqz&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Happy to answer questions in the comments — whether about &lt;br&gt;
the build, the tech stack, or anything else. 🚀&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
