Search by

julioserpone / gig-economics

julioserpone

Platform-agnostic, decoupled PHP library to measure fuel efficiency and the real profitability of a trip taken on Uber, DiDi, inDrive, Lyft or any ride-hailing platform.

Package info

github.com/julioserpone/gig-economics

pkg:composer/julioserpone/gig-economics

Statistics

Installs: 49

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-10-12 22:30 UTC

This package is auto-updated.

Last update: 2026-10-12 23:04:39 UTC


README

Tests Code Quality Latest Version License

A decoupled, platform-agnostic PHP library that answers the only question a ride-hailing driver really cares about: did this trip make money?

It computes energy economy, operating costs and the real profitability of a trip taken on Uber, DiDi, inDrive, Lyft — or any other marketplace — for combustion and electric vehicles alike, without talking to any platform, without a database, and without ever losing a cent to floating-point arithmetic.

use GigEconomics\Domain\Money\Money;
use GigEconomics\Domain\Money\Percentage;
use GigEconomics\Domain\Shared\Distance;
use GigEconomics\Domain\Shared\EnergyEconomy;
use GigEconomics\Domain\Shared\Platform;
use GigEconomics\Domain\Trip\Fare;
use GigEconomics\Domain\Trip\TripCostProfile;
use GigEconomics\Domain\Trip\TripProfitabilityCalculator;

$profile = TripCostProfile::forCombustion(
    pricePerGallon: Money::of('4.8384'),
    economy: EnergyEconomy::ofKilometresPerGallon('53.32'),
    depreciationPerKilometre: Money::of('0.15'),
    commissionRate: Percentage::of(25),
);

$result = TripProfitabilityCalculator::withDefaultTarget(Money::of('0.10'))
    ->calculate(
        Fare::create(Money::of('20.00'), Distance::ofKilometres(10), durationMinutes: 20),
        $profile,
        Platform::Uber,
    );

$result->netProfit()->format(); // $12.59
$result->verdict()->label();    // Accept

The same call for an electric vehicle

$electric = TripCostProfile::forElectric(
    pricePerKilowattHour: Money::of('0.30'),
    economy: EnergyEconomy::ofKilometresPerKilowattHour('6.00'),
    depreciationPerKilometre: Money::of('0.10'),
    commissionRate: Percentage::of(25),
);

$result = TripProfitabilityCalculator::withDefaultTarget(Money::of('0.10'))
    ->calculate(
        Fare::create(Money::of('20.00'), Distance::ofKilometres(12)),
        $electric,
        Platform::Uber,
    );

$result->breakdown->energyCost; // cost of the electricity consumed

Why this library exists

Drivers see a gross fare. What actually reaches their pocket is that fare minus the platform commission, minus the energy consumed, minus the kilometres added to the car. Gig Economics turns the gross number into the net one, and turns the decision of "should I accept this trip?" into a single call.

The design is deliberately stateless and storage-free: you pass data in, you get numbers out. There is no Eloquent, no migrations, no HTTP client.

Features

Money that actually adds up

Every amount is an immutable Money backed by brick/math arbitrary-precision decimals. Money::of('0.1')->plus(Money::of('0.2')) is exactly 0.3, and an invoice recomputed to the cent.

$subtotal = Money::of('8.158')->multipliedBy('2.8783'); // 23.4811714
$tax      = $subtotal->percentage(Percentage::of(15));  //  3.52217571
$total    = $subtotal->plus($tax);                      // 27.00334711

One energy model for combustion and electric vehicles

EnergyQuantity carries an amount together with its EnergyUnit (gallons, litres or kilowatt-hours), and EnergyEconomy carries distance-per-unit economy together with the same unit. A single API therefore serves a gasoline car (km/gal, km/L, MPG) and an EV (km/kWh, kWh/100 km).

EnergyEconomy::ofMilesPerGallon('25')->kilometresPerGallon();          // 40.23...
EnergyEconomy::ofKilometresPerGallon('10')->litresPer100Kilometres();  // 37.8541
EnergyEconomy::ofKilometresPerKilowattHour('6')->kilowattHoursPer100Kilometres(); // 16.6667

EnergyQuantity::ofLitres('3.785411784')->gallons(); // 1
EnergyQuantity::ofKilowattHours('42.5')->isElectric(); // true

Trip profitability with a verdict

TripProfitabilityCalculator folds commission, energy, depreciation, fixed costs and deadhead (unpaid) distance into a TripCostBreakdown, and returns an Accept / Marginal / Reject verdict plus the break-even fare the platform would have to pay for the trip to be worth driving.

$result->breakdown->energyCost;           // cost of the energy consumed
$result->breakdown->profitPerKilometre(); // net profit per km
$result->breakdown->margin();             // net margin as a fraction
$result->breakEvenFare();                 // the fare that makes net profit zero

Energy-log analytics

Feed an EnergyLog and get back the headline KPIs, monthly and time-of-day series, and vendor rankings — including the weighted price (a ratio of sums, never an average of prices):

$analytics = new EnergyAnalytics($log);

$analytics->kpis();                    // count, energy, spend, weighted price, avg economy
$analytics->weightedPrice();           // total spend / total energy
$analytics->averageEconomy();          // average of usable readings only
$analytics->monthlySeries();           // spend/energy/distance per month
$analytics->vendorRanking();           // vendors by spend, with weighted price
$analytics->cheapestVendors();         // vendors by price, cheapest first
$analytics->bestEconomyVendors();      // vendors with >= 2 readings only
$analytics->timeOfDayBreakdown();      // dawn / morning / afternoon / night
$analytics->spendConcentration(2);     // share of spend in the top-N vendors

Data-quality scoring

DataQualityAnalyzer flags the problems that quietly corrupt every driver's records: missing dates, missing or non-monotonic odometer readings, non-positive energy amounts, and statistical outliers detected with the median absolute deviation (robust against extreme values).

$analyzer = new DataQualityAnalyzer($log);

$analyzer->issues();            // every finding, with severity
$analyzer->hasErrors();         // are there blocking problems?
$analyzer->qualityScore();      // a single 0..1 health score
$analyzer->outlierEconomies();

Installation

composer require julioserpone/gig-economics

Requires PHP 8.3+ and brick/math. No other runtime dependency.

Reading a delimited export

EnergyLogImporter reads a header-driven, pipe-delimited (|) file so the same importer handles a gasoline log and an electric charging log. Fields are wrapped in double quotes and a UTF-8 BOM is tolerated; a pipe avoids the ambiguity of commas inside free-text fields such as vendor names.

use GigEconomics\Domain\Shared\EnergyType;
use GigEconomics\Support\EnergyLogImporter;

$log = (new EnergyLogImporter)->fromFile('fuel_log.psv');
// or, for an electric charging log:
$log = (new EnergyLogImporter)->fromFile('electric_log.psv', 0.0, EnergyType::Electric);
// or ->fromString($contents) / ->fromRows($rows)

Recognized input columns are date, time, vendor_tax_id, vendor_name, energy, unit_price, odometer, energy_type and tax_rate. Only input columns are read: any calculated column may also be exported and is ignored and recomputed by the domain, so a stale formula can never leak into your numbers.

Configuration

The defaults the library ships with (tax rate, default energy type, profit target) live in one immutable, framework-agnostic Configuration object instead of being hard-coded across the code. It never calls env() or a framework helper; you decide where the values come from.

use GigEconomics\Support\Configuration;
use GigEconomics\Support\EnergyLogImporter;
use GigEconomics\Support\TripProfitabilityFactory;

// 1. Built-in defaults, no environment access:
$configuration = Configuration::defaults();

// 2. From an explicit array — a Laravel config file, a Symfony parameter bag,
//    a plain PHP array from a CLI, whatever you use:
$configuration = Configuration::fromArray([
    'tax_rate_percent' => 12.0,
    'default_energy_type' => 'electric',
    'target_profit_per_kilometre' => '0.20',
]);

// 3. From the process environment (GIG_ECONOMICS_TAX_RATE, ...):
$configuration = Configuration::fromEnvironment();

// Configuration is immutable — with*() returns a new instance.
$configuration = Configuration::defaults()->withTaxRatePercent(0.0);

// The importer and the calculator factory consume it; an explicit call
// argument always overrides the configured default.
$log = (new EnergyLogImporter($configuration))->fromFile('fuel_log.psv');
$calculator = TripProfitabilityFactory::fromConfiguration($configuration)->calculator();

Using it from Laravel

Because the object is plain PHP, a Laravel app can publish its own config file and hand it to the library — the package itself never depends on Laravel:

// config/gig-economics.php (in your application, not in this package)
return [
    'tax_rate_percent' => env('GIG_ECONOMICS_TAX_RATE', 15.0),
    'default_energy_type' => 'regular_gasoline',
    'target_profit_per_kilometre' => '0.15',
];

// elsewhere
$configuration = Configuration::fromArray(config('gig-economics'));
Key Default Meaning
tax_rate_percent 15.0 Tax applied to each purchase subtotal
default_energy_type regular_gasoline Energy type assumed when the file omits it
target_profit_per_kilometre 0.15 Default profit target for the verdict

The equivalent environment variables are GIG_ECONOMICS_TAX_RATE, GIG_ECONOMICS_DEFAULT_ENERGY_TYPE and GIG_ECONOMICS_TARGET_PROFIT_PER_KILOMETRE.

Architecture

src/
├── Domain/
│   ├── Money/        Currency, Money, Percentage          (value objects)
│   ├── Shared/       Distance, EnergyUnit, EnergyType, EnergyQuantity,
│   │                 EnergyEconomy, Platform, TimeOfDay
│   ├── Energy/       EnergyPrice, EnergyPurchase, EnergyLog
│   ├── Trip/         Fare, TripCostProfile, TripCostBreakdown,
│   │                 TripProfitabilityCalculator, TripVerdict
│   ├── Analytics/    EnergyAnalytics, EnergyKpis, VendorSummary, PeriodSummary
│   └── Quality/      DataQualityAnalyzer, DataQualityIssue, Severity
├── Support/          EnergyLogImporter, Configuration,
│                     TripProfitabilityFactory              (the only I/O)
└── Exceptions/       CurrencyMismatchException, InvalidDomainValueException

The core is pure domain code: no I/O, no framework, no global state. The Support layer is the only place that touches the outside world, which keeps every calculation trivially testable and portable to any PHP project — Laravel, Symfony, a CLI, or a long-running worker.

Design principles

  • Immutability everywhere. Every value object is readonly; operations return new instances. Reused profiles can never be mutated by a calculation.
  • No floats in money math. Amounts travel as BigDecimal and are rounded only for presentation.
  • Invariants enforced at the boundary. A negative distance, a zero-energy division, a mismatched unit or an unknown currency fails fast and loudly.
  • Drivetrain-agnostic. The energy unit travels with the value, so the same calculation serves combustion and electric vehicles without branching on caller convention.
  • Platform-agnostic. Platform ships defaults for Uber, DiDi, inDrive and Lyft, and Platform::Custom lets you model any other marketplace.
  • Framework-agnostic configuration. Defaults live in one immutable Configuration object that reads from an array or the environment; the pure domain never reads it, it always receives explicit values.

Testing

composer test      # Pest 3/4 test suite
composer lint      # Laravel Pint
composer analyse   # PHPStan level max (with strict rules)
composer ci        # all of the above, in order

The suite pairs fast unit tests with integration tests that parse realistic fixtures and assert the documented KPIs for both a combustion log and an electric charging log.

Contributing

Commits follow Conventional Commits and arrive through a <type>/<summary> branch — never directly on main. The full workflow is documented in docs/CONTRIBUTING.md. composer ci must pass before a change is merged; the same script runs in CI across PHP 8.3, 8.4 and 8.5 against both lowest and highest dependency versions. AI assistants should follow the guidance in AGENTS.md.

License

MIT — see LICENSE.