Dansday

3cat.my

3cat.my

3cat sells refurbished Apple devices in Malaysia. The shop is Laravel, built on Bagisto, and I have been working on it since 20 December 2023. I committed to it yesterday. This is the longest continuous engagement of my career and the codebase where I have made the most mistakes in public, so it is the page where being accurate matters most.

Repository        3cat-Sdn-Bhd/3cat (private), created 5 December 2023
My first commit   20 December 2023
Most recent       27 August 2026
Commits on main   2,722 total, 927 of them mine
Contributors      8 or more; I am the largest single one at 34%
Pull requests     559 of mine merged, 59 closed without merging
Repo size         86 MB tracked, 278 MB of it vendored framework
My commits by year    2023   28
                      2024  356
                      2025  173
                      2026  367   (to 27 August)

What this page used to claim

The previous version of this page listed six achievements with numbers attached: a threefold lift in Q4 2024 sales, invalid submissions cut by 97 percent, incident response time cut by 99 percent, database load down 95 percent, a 40 percent lift in key performance indicators.

I cannot verify any of those from the repository, because business outcomes are not in git. Some of them may well be true — I remember the reservation confirmation step genuinely stopping a flood of accidental orders. But "99 percent" and "95 percent" are stated with a precision that no source I can reach supports, and a page that presents unverifiable figures next to verifiable work makes the verifiable work look like marketing too. So they are gone. What follows is what the diffs show, including the parts that are unflattering.

One claim from the old page survives, and it is the one that admitted a cost: that I shipped a cart discount field in a day by cutting the QA cycle, accepting a higher bug risk for the revenue. That is true, it is the first entry in a pattern this page is largely about, and I will come back to it.

I did not start this codebase

The first commit is not mine. The repository was created on 5 December 2023 by another engineer, with a continuous integration config added the same day. I arrived fifteen days later, and my first commit is titled IP-28437: PDP Core.

That prefix matters. In 2023 and through much of 2024 my tickets here carried an IP- number, because I was working on this shop through iPrice. By 2025 the prefix is gone and every ticket is a bare number in 3cat's own sequence. Nobody wrote a commit explaining that transition; you can only see it in the shape of the subject lines. The relationship changed and the ticket numbering changed with it.

So this is a shared codebase with at least eight contributors, and I am the largest of them without having designed it. Almost everything below is a decision made inside constraints somebody else set, which is the normal condition of commercial software and not something I want to write around.

A note on my own numbers: I nearly published a lower count for this page, because I have committed under two names and three email addresses over three years and my first query only caught one of them. 927 is the figure after finding the rest. If I can undercount my own work by a quarter, I can be wrong about anything else here too, which is why every number on this page came from a command rather than from memory.

The framework lives inside the repository

Bagisto is not a dependency here. It is vendored into the tree at 3cat/packages/Webkul — 15,934 files, 2,117 of them PHP, 278 MB of the repository's 86 MB tracked footprint once you account for history.

The consequence shows up every time upstream moves. An upgrade is not a version bump, it is a hand-resolved merge against a framework somebody else keeps changing. In February 2026 an upgrade broke an administrative screen, and the fix was to copy one more upstream file into our tree — which resolves the immediate problem and enlarges the thing that caused it. Every file copied in is a file that will never receive an upstream fix again unless somebody copies that too.

This has been a standing cost since July 2024. I have not solved it. I am not sure it is solvable from where the project now stands, and pretending there is a clean answer would be worse than saying that.

The thread that runs through everything: is the number on the screen true

If this engagement has one subject, it is this. Nearly three years of it, in order:

December 2023. A product page showing a discount that had been hardcoded, not calculated. My first month.

November 2024. A "Low Stock" badge rendering on products whose quantity was zero. Not low. None.

March 2025. A "Rare Gem!" label, applied by a rule that had nothing to do with rarity.

June 2025. Pre-order delivery commitments displayed as service-level promises the operation had not agreed to.

January 2026. "Great Condition" replaced with "56-step checks" — a claim about process rather than a claim about the item, and a real improvement, because a process claim can be audited and an adjective cannot.

February 2026. Then this. A display_quantity attribute was added, so "Only 3 left!" became a field somebody types rather than a count of anything. It is still there today, alongside a companion attribute for marking an item a hot take. The urgency on the page is now editorial.

July 2026. The product page switched to showing the price after an automatically applied delivery voucher — a lower, more attractive figure, and one the customer genuinely pays when the voucher applies to them. The same change had a second half that computed "from RM X" on listing pages using only variants you can actually buy, rather than any variant including the sold-out cheap one. That half was reverted and has not come back.

I used to think this thread was a story about progress. It is not. It oscillates. The same month can make one number more honest and another number less honest, and the pressure that produces the dishonest ones — a promotion calendar, a conversion target, a competitor's page — never goes away. The most useful thing I have learned here is to notice which direction a change moves the claim, because nobody's ticket ever says "make this less true".

Vouchers: nine bugs, then a tool

Conditional discounts have produced a bug in nine separate months of this project. In order: a hand-rolled condition evaluator in October 2024; an operator comparing the wrong way round in December 2024; a revert in April 2025; a cascade in the XOX telco mechanism in July 2025; a helper introduced in December 2025 to stop the duplication; stackable vouchers patched twenty-two minutes after the merge that broke them in January 2026; an exclamation mark missing from a client-side allow-list in February 2026, next to a framework validator that returned an undefined variable for the same input; a first() call in June 2026 that only became wrong once the feature was used as designed; and in July 2026 a delivery voucher outliving the delivery it discounted after the customer switched to reservation.

The through-line is that discounts here are conditional on cart state, and for most of those nine months nothing owned the job of re-deriving them when the state changed. Every fix was a listener or a guard for one transition.

What exists now is more interesting than any of the fixes: a controller whose entire job is to preview which orders a cart rule's conditions would match, before you save the rule. After nine months of discovering condition logic in production, the codebase grew a way to look at it first. That is the correct ending, and it cost nine bugs to arrive at.

The cache I built, and its complete absence today

In November 2024 I built application-level response caching. In May 2025 it had to be rate-limited. In September 2025 it was largely replaced by a ten-minute cache at the edge, alongside an open pull request deleting 1,151 lines. In January 2026 the last two routes quietly lost their cache middleware inside an unrelated tidy-up, which I only noticed while writing about that month.

Today there is not a single reference to that middleware anywhere in the codebase. I checked before writing this sentence, expecting to find a couple of stragglers. Zero.

An edge cache with a short time-to-live replaced a year of my invalidation logic, and it works better, because invalidation is the hard part and a TTL sidesteps it entirely. In July 2026 I used that fact to argue for moving promotion rendering back into the page — work inside a cached render is amortised across every hit — and that change was reverted for reasons the repository does not record. I still think it was right. It is also gone.

The honest summary is that the most sophisticated system I built for this shop was retired in favour of something simpler that someone else could operate, and that is usually what should happen.

The bug I keep writing, and have not fixed

Five times in this codebase I have assumed data was present when it was not: an array's first category in March 2024, a first() call in July 2024, a find() in December 2024, a JSON key in February 2025, and a set of orWhereNotNull filters in January 2026.

Those last ones are the point of this section. In January 2026 I found three of them in the order administration grid, wrote them up as unfixed, and published that. They are still there. I opened the file while writing this page: three orWhereNotNull calls, on a point-of-sale identifier, a shipment timestamp and a coupon code. Each turns a filter that should narrow results into one that widens them, so a staff member filtering orders gets rows that do not match what they asked for, silently, with no error.

Seven months of knowing, writing it down publicly, and not fixing it. I have no explanation that reflects well on me. It is a small bug in a screen only staff see, it never shouts, and there has always been something with a ticket number in front of it. That is exactly how this class of defect survives, and being able to describe the mechanism has evidently not been enough to make me act on it.

Money, and the gap between two state machines

The most consequential work here is always about payment, and August 2026 was almost entirely that: the reservation and deposit flow, ticket 2139 and its follow-ups, across three weeks.

The one to read is a hotfix from 12 August, titled Paid Reservation orders bypass payment gateway and are marked Ready to Process. Orders were reaching a state that means "the money is in, begin fulfilment" without going through the gateway. The fix is three files and thirty-three lines: a guard in the order controller, a change to the order listener, and two lines in the cart's front-end controller.

That is the shape of nearly every serious bug I have fixed here. An order has a state machine. Money has a state machine. They are separate, they are updated from different places, and the bugs live in the space between them. July 2026's version was a transaction row written the moment a customer was redirected to pay, describing a payment that had not happened and, if they abandoned the page, never would. The fix there was deleting the method that created it, and making the callback write a row only for a paid status.

Both fixes are subtractions. That keeps being true: the good changes in this codebase are usually removals — 810 lines of an unused sales reporting module, 345 lines of an API controller, a hardcoded currency pulled out of twenty-four files, a method that recorded intentions as facts.

What actually got better, in one commit

On 19 August 2026 I deleted two workflow files that ran an AI reviewer on every pull request, ninety-four lines of them, and in the same commit added a forty-five line workflow that runs the tests. That same week has commits titled Fix 1 unit test and Fixing test unit.

I had put the AI reviewer in place in April. Four months later I replaced a machine that comments with a machine that fails the build, which is the trade I would recommend to anybody. An automated reviewer on a solo-merged repository produces reading material. A test suite in the merge path produces a decision.

The same day carries a commit titled Strip comments — forty-two deletions across six files. Consistent with how I prefer to work, and worth noting that I applied it to a shared codebase, which is a preference imposed on colleagues rather than a purely personal one.

Two things I will not describe here

In December 2025 I found and reported a vulnerability in the framework's installer. I have written about it at the level of what class of problem it was and nothing more — no routes, no conditions, no mechanism — because the framework is used by other shops and some of them will not have updated.

And in July 2026 two infrastructure pull requests added material to this repository that should never be in one. I am not going to be more specific than that. The repository is private, but a private repository is an access boundary, not a secret store, and it has been done this way since the project's first commit in December 2023, so it is a standing practice rather than a July mistake. The remedy is unglamorous: rotate, move the material into something the deployment reads at boot, and treat the history as compromised rather than assume nobody looked.

The small things that never end

The browser back button has been broken and fixed in five separate months. The chat call-to-action button has been changed in more months than any other single element on the site. The exit-intent popup has taken eight pull requests, with triggers removed one at a time and then added back; it is still in the codebase today, its own component, its own script, its own stylesheet.

These are not failures of attention. They are what happens when a marketing surface has several owners and no written contract about what it should do. The engineering fix is not in the code, and I have never managed to get the non-code fix agreed.

What nearly three years on one codebase taught me

That the interesting problems are not technical. The orWhereNotNull bug is a five-minute fix that has survived seven months of me knowing about it. "Only 3 left!" became a text field because somebody needed urgency and the honest number would not provide it. Nine voucher bugs happened because conditional pricing had no owner, not because conditions are hard. The cache I was proudest of was retired for a ten-minute TTL that a colleague can reason about at three in the morning.

That a vendored framework is a debt you service forever, and the payments are invisible until an upgrade.

That deletions are the honest measure of a codebase's health. A shop that only grows is a shop where nobody has the standing to say a thing was a mistake, and I can point at the removals here as the clearest evidence that this one is healthy.

And that being able to describe a problem accurately is not the same as fixing it. This page is more precise about the state of that order grid than any ticket in the tracker, and the code has not changed. If I take one thing from writing it, that is the thing.

Stack

Framework    Laravel with Bagisto, vendored into the repository
Language     PHP
Front end    Blade, Tailwind CSS, Alpine-style JS controllers
Data         MySQL
Payments     multiple gateways, including eGHL; reservation and deposit flows
Caching      edge cache with a short TTL; no application-level cache remains
Integrations telco partner mechanism, webhooks with attribution passthrough,
             an authenticated machine-readable stock surface with its own keys,
             a workflow automation consumer, a warranty portal on Supabase
CI           tests in the merge path since August 2026