Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

Date and Deadline Mistakes That Quietly Break Business Apps

About Post

Date bugs are polite. They don't crash your app on the day you ship. They wait.

They wait for the 31st of the month, for the 29th of February, for a user in a different time zone, for the last day of a contract at ten in the morning. Then they quietly produce a wrong date, which turns into a wrong invoice, a wrong reminder, or a tenant being told their lease has expired when it hasn't.

Business apps (contracts, rent, subscriptions, HR leave, SLAs) are full of dates, so they're full of these. Here are the seven I see most often, why each happens, and the fix. Examples use Carbon in Laravel 12, but the traps are the same in any language.

1. Treating an end date like a timestamp

The mistake: a one-year contract runs from 1 January to 31 December. It's stored as ends_at = 2026-12-31 00:00:00 and checked with $contract->ends_at->isPast().

Why it breaks: on 31 December at 10am, midnight on the 31st is already in the past. The contract shows as expired for its entire last day.

The fix: business dates are dates, not moments. Store them in a date column, compare them to today's date, and decide once whether the end is inclusive or exclusive. Then name the column so nobody has to guess:

// Inclusive end date: the contract is active ON ends_on
Contract::whereDate('starts_on', '<=', today())
    ->whereDate('ends_on', '>=', today())
    ->get();

// One-year contract from 2026-01-01 ends on 2026-12-31
// (immutable dates assumed, see mistake 6)
$endsOn = $startsOn->addYear()->subDay();

Some teams prefer an exclusive end (ends_before = 2027-01-01, compared with <), which makes durations easier to calculate. Either works. Mixing both in one codebase is what causes the off-by-one.

2. Adding months and trusting the result

The mistake: a contract starts on 31 January, and the next rent due date is $start->addMonth().

Why it breaks: there is no 31 February. By default, Carbon (like PHP's DateTime) overflows into the next month, so you get 3 March in a normal year.

$start = CarbonImmutable::parse('2026-01-31');

$start->addMonth();              // 2026-03-03  (overflow)
$start->addMonthNoOverflow();    // 2026-02-28  (clamped to month end)

The fix: use the NoOverflow variants for anything a human calls "monthly". And watch the sneaky second half of this bug, next.

3. Chaining from the previous due date

The mistake: each due date is calculated from the previous one: Jan 31, then Feb 28, then Mar 28, then Apr 28...

Why it breaks: once the date gets clamped to the 28th, it stays there. The schedule drifts away from the 31st forever.

The fix: always calculate from the original anchor date, not the previous result:

$dueDates = collect(range(0, 11))->map(
    fn (int $n) => $start->addMonthsNoOverflow($n)
);
// Jan 31, Feb 28, Mar 31, Apr 30, May 31, ...

4. Forgetting that years aren't 365 days

The mistake: "one year later" written as addDays(365), or annual renewals for a contract that starts on 29 February.

Why it breaks: a leap year has 366 days, so addDays(365) lands a day early. And a 29 February start has no matching date in most years. CarbonImmutable::parse('2028-02-29')->addYear() overflows to 1 March 2029, while addYearNoOverflow() gives 28 February.

The fix: add calendar units (addYear, addMonths), not day counts, and decide explicitly what a 29 February anniversary means for your business. Then write a test for it, because it only comes around every four years.

5. Asking "what day is it?" in the wrong time zone

The mistake: the server runs in UTC, the business runs in Doha (UTC+3). A payment deadline is "end of day, 5 October", checked with now()->toDateString().

Why it breaks: for the first three hours of every Doha day, the server still thinks it's yesterday. Late fees, reminders and "due today" lists are all shifted at the edges.

The fix: store timestamps in UTC, but compute business dates in the business's time zone, explicitly:

$businessToday = now('Asia/Qatar')->toDateString();

// Scheduled jobs too: run at 8am local time, not 8am UTC
Schedule::command('reminders:send')
    ->dailyAt('08:00')
    ->timezone('Asia/Qatar');

If your users span several time zones, a deadline needs a time zone attached to it. "5 October" alone isn't a moment in time.

6. Mutating a date you meant to keep

The mistake:

$start = $contract->starts_on;   // a mutable Carbon instance
$end = $start->addYear();        // surprise: $start changed too

Why it breaks: by default, Laravel's date casts give you mutable Carbon objects. addYear() changes the object itself and returns it, so $start and $end are now the same date. Any code that uses $start afterwards is wrong.

The fix: use immutable dates everywhere. One line in a service provider's boot() method does it app-wide:

use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\Date;

Date::use(CarbonImmutable::class);

Every method now returns a new instance and leaves the original alone. (For a single spot, $start->copy()->addYear() works too.)

7. Trusting diffInDays() to return a neat integer

The mistake: showing "days left" as now()->diffInDays($contract->ends_on).

Why it breaks: since Carbon 3 (which Laravel 12 uses), diffIn* methods return a float and are signed by default. Comparing now() (2:30pm) to a date at midnight gives something like 29.4, and a date in the past gives a negative number. Code written for Carbon 2, which returned absolute integers, now shows odd values.

The fix: compare dates to dates, and say what you mean:

$daysLeft = (int) today()->diffInDays($contract->ends_on->startOfDay());
// positive: days remaining; 0: ends today; negative: already ended

And remember the counting question: from 1 to 10 October is a difference of 9 days, but 10 days if both ends count. Contracts, leave requests and SLAs often mean the inclusive version.

The date rule: decide your conventions once (date vs timestamp, inclusive vs exclusive end, which time zone defines "today", immutable everywhere), write them down, and test the edges: the 31st, 29 February, midnight, and the last day of a contract.

A tiny test suite worth having

Date bugs are easy to test once you freeze time. In Laravel, $this->travelTo(...) lets a test pretend it's 31 December at 10am, or 1am in Doha. A handful of tests for the edge dates above will catch most of these before a customer does.

Which date bug has caught you? I'd bet on month-end overflow, but time zones are a strong second.

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close