Add automated tests for the 25% pricing formula and $25/$15 shipping thresholds

Launch gate (design doc §5/§8) requires automated tests for both; the repo
had zero test infra until now. PHPUnit + Brain Monkey, run via `make test`
against the existing wordpress dev container.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MqhgnCRjjVLFnZ3Jpn93Vg
This commit is contained in:
twooeyandClaude Sonnet 5 committed 2026-08-28 10:58:29 -04:00
1 parent a733ba4bfd
commit 5d8533f290
9 files changed
+2143 -1

No files matched your search

@@ -0,0 +1,21 @@
<?php
namespace Bookstore\Core\Tests;
use Brain\Monkey;
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
abstract class TestCase extends PHPUnitTestCase
{
protected function setUp(): void
{
parent::setUp();
Monkey\setUp();
}
protected function tearDown(): void
{
Monkey\tearDown();
parent::tearDown();
}
}
@@ -0,0 +1,65 @@
<?php
namespace Bookstore\Core\Tests\Unit\Pricing;
use Bookstore\Core\Pricing\PricingEngine;
use Bookstore\Core\Tests\TestCase;
/**
* Launch gate: "25% pricing logic ... have automated tests" (design doc
* §5/§8). calculate_retail_price()/round_to_99()/estimate_processing_cost()
* are pure math with no WordPress/WooCommerce dependency, so no mocking
* is needed here.
*/
class PricingEngineTest extends TestCase
{
/**
* @dataProvider retailPriceCases
*/
public function test_calculate_retail_price_applies_25_percent_margin_formula(
float $supplier_base,
float $supplier_shipping,
float $expected
): void {
$actual = PricingEngine::calculate_retail_price($supplier_base, $supplier_shipping);
$this->assertEqualsWithDelta($expected, $actual, 0.001);
}
public static function retailPriceCases(): array
{
return [
'small order' => [10.00, 3.00, 18.99],
'no supplier shipping' => [5.00, 0.00, 7.99],
'near a .99 boundary' => [14.20, 0.05, 20.99],
'zero supplier cost still covers fixed processing' => [0.00, 0.00, 0.99],
'larger order' => [100.00, 10.00, 152.99],
];
}
/**
* @dataProvider roundTo99Cases
*/
public function test_round_to_99_never_rounds_below_the_input(float $price, float $expected): void
{
$this->assertEqualsWithDelta($expected, PricingEngine::round_to_99($price), 0.001);
}
public static function roundTo99Cases(): array
{
return [
// The case the class's own docblock calls out: the old
// ceil($price) - 0.01 formula gave 20.99 here, which is
// below the $20.995 input and would erode the target margin.
'fractional price above the X.99 ending' => [20.995, 21.99],
'exact integer input' => [18.00, 18.99],
'already exactly X.99' => [7.99, 7.99],
'just above X.99' => [8.00, 8.99],
];
}
public function test_estimate_processing_cost(): void
{
$this->assertEqualsWithDelta(0.83, PricingEngine::estimate_processing_cost(20.99), 0.001);
$this->assertEqualsWithDelta(3.00, PricingEngine::estimate_processing_cost(100.00), 0.001);
}
}
@@ -0,0 +1,74 @@
<?php
namespace Bookstore\Core\Tests\Unit\Shipping;
use Bookstore\Core\Shipping\BSC_Free_Shipping;
use Bookstore\Core\Tests\TestCase;
use Brain\Monkey\Functions;
use Mockery;
/**
* Launch gate: "$25/$15 shipping thresholds have automated tests" (design
* doc §5/§8). threshold_for_current_customer() reads membership via a raw
* $wpdb query and thresholds via get_option() (with the $25/$15 launch
* numbers as the fallback defaults) — both are stubbed here rather than
* hitting WordPress/a real database. The class's own docblock says
* thresholds are option-backed "so the launch gate's automated test can
* assert against the same source this reads from" — the last test below
* proves exactly that.
*/
class BSCFreeShippingTest extends TestCase
{
protected function tearDown(): void
{
global $wpdb;
$wpdb = null;
parent::tearDown();
}
public function test_guest_gets_default_25_dollar_threshold(): void
{
Functions\when('get_current_user_id')->justReturn(0);
Functions\when('get_option')->alias(fn($name, $default) => $default);
$this->assertSame(25.00, BSC_Free_Shipping::threshold_for_current_customer());
}
public function test_logged_in_non_member_gets_default_25_dollar_threshold(): void
{
Functions\when('get_current_user_id')->justReturn(42);
Functions\when('get_option')->alias(fn($name, $default) => $default);
$this->mock_wpdb_is_free_member(false);
$this->assertSame(25.00, BSC_Free_Shipping::threshold_for_current_customer());
}
public function test_free_member_gets_15_dollar_threshold(): void
{
Functions\when('get_current_user_id')->justReturn(7);
Functions\when('get_option')->alias(fn($name, $default) => $default);
$this->mock_wpdb_is_free_member(true);
$this->assertSame(15.00, BSC_Free_Shipping::threshold_for_current_customer());
}
public function test_custom_option_override_is_respected_not_the_hardcoded_default(): void
{
Functions\when('get_current_user_id')->justReturn(0);
Functions\expect('get_option')
->once()
->with('bsc_shipping_threshold_guest', 25.00)
->andReturn(29.99);
$this->assertSame(29.99, BSC_Free_Shipping::threshold_for_current_customer());
}
private function mock_wpdb_is_free_member(bool $is_member): void
{
global $wpdb;
$wpdb = Mockery::mock('wpdb');
$wpdb->prefix = 'wp_';
$wpdb->shouldReceive('prepare')->once()->andReturn('SELECT is_free_member ...');
$wpdb->shouldReceive('get_var')->once()->andReturn($is_member ? '1' : '0');
}
}
@@ -0,0 +1,22 @@
<?php
/**
* Unit-test bootstrap: pure PHPUnit + Brain Monkey, not a full WordPress
* bootstrap. includes/*.php all guard on `defined('ABSPATH') || exit;`
* at file-load time, and BSC_Free_Shipping extends \WC_Shipping_Method
* (a WooCommerce core class) — both need to exist before Composer's
* autoloader ever pulls those files in, hence defining/stubbing them
* here rather than per test case.
*/
require __DIR__ . '/../vendor/autoload.php';
if (!defined('ABSPATH')) {
define('ABSPATH', __DIR__ . '/');
}
if (!class_exists(\WC_Shipping_Method::class)) {
class WC_Shipping_Method
{
}
}