Files
autocrm/tests/Feature/Audit/ShopJourneyE2ETest.php
T
Vasyka 0620635abb test: full E2E audit + fix CsvImportExport vehicle.brand → make
Audit pass (33 new tests in tests/Feature/Audit/):
- CrmFunnelE2ETest: full Lead→Deal→Appointment→WO→Payment journey,
  covering 5 previously-untested models. Verifies WO.balanceDue updates
  correctly after payments, including refunds (negative amount → balance
  increases).
- WorkOrderTotalsTest: works+parts+subcontract+discount sum correctly,
  cancelled subcontract excluded, deleting lines triggers recalc, status=done
  consumes part reservations into issues, cancelled releases reservations.
- ShopJourneyE2ETest: register→cart→checkout→email confirmation→tracking
  page reachable→admin fulfills→stock drops→warehouse event recorded.
  Also guest checkout still works without account.
- CsvImportExportTest: round-trip, dedup-by-phone, **caught real bug** —
  Vehicle export wrote $row->brand (no such property) and import set
  'brand' => row['brand'] in Vehicle::create (column is `make`). Fix
  applied to both paths.
- TenantBackupServiceTest: zip contains valid manifest with counts +
  data/*.json per model + works embedded with WorkOrder.
- WorkOrderPdfServiceTest: generated PDF starts with %PDF, includes WO data,
  non-trivial size, handles empty WO.
- PayrollCalculatorTest: base + works_pct + parts_pct + bonus - fine -
  advance, scoped to user + period.
- NotificationFallbackTest: Telegram wins when chat_id present, falls back
  to email when not, returns false when neither, tenant disable flag stops
  both.
- AiProvidersCrossCheckTest: OpenAI request shape, Gemini URL with model,
  no-key friendly message, tenant model override propagates into HTTP body.
- SettingsPersistenceTest: 25-key settings JSON round-trips, partial update
  via array_replace_recursive preserves other keys.
- CompanyProvisionerTest: suspend / reactivate / archive behavior.

Bug fixed: CsvImportExport used `brand` on Vehicle which has column `make`.
The export silently emitted empty values, the import silently dropped the
brand. Now both paths use `make`.

Full suite: 173 passed (468 assertions). 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 07:05:46 +00:00

155 lines
5.7 KiB
PHP

<?php
namespace Tests\Feature\Audit;
use App\Mail\ShopOrderConfirmationMail;
use App\Models\Central\Company;
use App\Models\Central\Plan;
use App\Models\Tenant\OnlineOrder;
use App\Models\Tenant\Part;
use App\Models\Tenant\ShopCustomer;
use App\Models\Tenant\Warehouse;
use App\Models\Tenant\WarehouseEvent;
use App\Services\Warehouse\WarehouseService;
use App\Tenancy\TenantManager;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
/**
* End-to-end shop journey: customer registers → adds to cart → checks out →
* receives confirmation email → admin fulfills → WarehouseService.issue runs →
* stock decreases → tracking page reachable.
*
* Exercises ShopAuthController, ShopController, OnlineOrder, OnlineOrderItem,
* ShopCustomer, ShopOrderNotifier, WarehouseService, and the tracking route.
*/
class ShopJourneyE2ETest extends TestCase
{
use RefreshDatabase;
public function test_full_shop_journey_registered_customer(): void
{
$ctx = $this->bootShop('e2e-shop');
$svc = app(WarehouseService::class);
// Stock a published part: 20 in inventory.
$part = Part::create([
'name' => 'Plăcuțe BMW', 'article' => 'BMW-001', 'brand' => 'TRW',
'sell_price' => 250, 'buy_price' => 150,
'qty' => 0, 'unit' => 'buc',
'is_active' => true, 'is_published' => true,
]);
$svc->receive($part, 20, 150);
$this->assertEquals(20.0, (float) $part->fresh()->qty);
Mail::fake();
$base = 'http://e2e-shop.service.mir.md';
// 1. Register.
$this->post("$base/shop/register", [
'name' => 'Tester', 'phone' => '+37377012345',
'email' => 'tester@example.com',
'password' => 'secret123', 'password_confirmation' => 'secret123',
])->assertRedirect('/shop/account');
$customer = ShopCustomer::where('phone', '+37377012345')->first();
$this->assertNotNull($customer);
$this->assertTrue(Auth::guard('shop')->check());
// 2. Browse catalog (anonymous-safe, but we're logged in).
$r = $this->get("$base/shop");
$r->assertOk();
$r->assertSee('Plăcuțe BMW');
// 3. Add to cart.
$this->post("$base/shop/part/{$part->id}/add", ['qty' => 2])
->assertRedirect('/shop/cart');
$this->get("$base/shop/cart")->assertOk()->assertSee('500.00');
// 4. Checkout.
$this->get("$base/shop/checkout")
->assertOk()
->assertSee('Tester', false) // prefilled from logged-in customer
->assertSee('+37377012345');
$this->post("$base/shop/checkout", [
'customer_name' => 'Tester',
'customer_phone' => '+37377012345',
'customer_email' => 'tester@example.com',
'delivery_method' => 'pickup',
])->assertRedirect();
$order = OnlineOrder::first();
$this->assertNotNull($order);
$this->assertEquals($customer->id, $order->shop_customer_id);
$this->assertEquals(500.0, (float) $order->total);
// 5. Confirmation email sent to customer.
Mail::assertSent(ShopOrderConfirmationMail::class, fn ($m) => $m->order->id === $order->id);
// 6. Tracking page reachable by token (NO auth).
Auth::guard('shop')->logout();
$r = $this->get($order->trackingUrl());
$r->assertOk();
$r->assertSee('#' . $order->number);
$r->assertSee('Plăcuțe BMW');
// 7. Admin (back in tenant context) fulfills via service::issue.
$svc->issue($part, 2, null, $order, 'shop fulfill');
$part->refresh();
$this->assertEquals(18.0, (float) $part->qty, 'stock decreased by 2');
// 8. The warehouse event records the issue with ref pointing at the order.
$event = WarehouseEvent::where('part_id', $part->id)
->where('type', 'issue')
->latest('id')->first();
$this->assertNotNull($event);
$this->assertEquals(-2.0, (float) $event->qty_delta);
$this->assertStringContainsString('OnlineOrder', (string) $event->ref_type);
}
public function test_guest_checkout_still_works_without_account(): void
{
$this->bootShop('guest');
$part = Part::create([
'name' => 'Filtru', 'sell_price' => 50, 'qty' => 5, 'unit' => 'buc',
'is_active' => true, 'is_published' => true,
]);
$base = 'http://guest.service.mir.md/shop';
$this->post("$base/part/{$part->id}/add", ['qty' => 1])->assertRedirect();
$this->post("$base/checkout", [
'customer_name' => 'Guest', 'customer_phone' => '+37399999999',
'delivery_method' => 'pickup',
])->assertRedirect();
$order = OnlineOrder::first();
$this->assertNotNull($order);
$this->assertNull($order->shop_customer_id);
$this->assertEquals(50.0, (float) $order->total);
}
private function bootShop(string $slug): array
{
$plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]);
$company = Company::create([
'plan_id' => $plan->id, 'slug' => $slug,
'name' => ucfirst($slug), 'status' => 'active',
'settings' => ['shop' => ['enabled' => true, 'delivery_methods' => ['pickup']]],
]);
app(TenantManager::class)->setCurrent($company);
$wh = Warehouse::create([
'code' => 'MAIN', 'name' => 'Main',
'is_default' => true, 'is_active' => true,
]);
$company->forceFill(['default_warehouse_id' => $wh->id])->saveQuietly();
return compact('company');
}
}