Files
ladill-pos/tests/Feature/PosProductImportTest.php
T
isaaccladandClaude Opus 4.8 9fcea24680
Deploy Ladill POS / deploy (push) Successful in 1m39s
Add bulk CSV product import for POS (retail + restaurant)
Customers with large catalogs can upload a CSV instead of adding products one by
one. Mode-aware: restaurant imports into the local catalog (chunked INSERT,
categories resolved/created by name); retail batches to the CRM products/bulk
endpoint. Streamed parsing + batched writes handle thousands of rows in one
request. Includes a downloadable template, an Import button on both product
pages, and a skipped-rows report.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 09:20:40 +00:00

107 lines
4.0 KiB
PHP

<?php
namespace Tests\Feature;
use App\Http\Middleware\EnsurePlatformSession;
use App\Models\PosCategory;
use App\Models\PosLocation;
use App\Models\PosProduct;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class PosProductImportTest extends TestCase
{
use RefreshDatabase;
private function user(): User
{
return User::create([
'public_id' => 'u-'.uniqid(),
'name' => 'Owner',
'email' => uniqid().'@example.com',
]);
}
protected function setUp(): void
{
parent::setUp();
$this->withoutMiddleware(EnsurePlatformSession::class);
$this->withoutVite();
config(['crm.url' => 'https://crm.test/api', 'crm.key' => 'test-crm-key']);
}
private function csv(string $content): UploadedFile
{
return UploadedFile::fake()->createWithContent('products.csv', $content);
}
public function test_restaurant_import_creates_local_products_and_categories(): void
{
$user = $this->user();
PosLocation::create(['owner_ref' => $user->public_id, 'name' => 'Main register', 'currency' => 'GHS', 'service_style' => 'restaurant']);
$csv = "name,sku,price,category,active\n"
."Coffee,CFE-01,15.00,Drinks,yes\n"
."Latte,LAT-02,18.50,Drinks,yes\n"
."Croissant,,8.00,Pastry,no\n"
.",NONAME,5.00,Drinks,yes\n" // skipped: no name
."Bad,BAD-1,notaprice,Drinks,yes\n"; // skipped: bad price
$this->actingAs($user)
->post(route('pos.products.import'), ['file' => $this->csv($csv)])
->assertRedirect(route('pos.products.index'))
->assertSessionHas('success');
$this->assertSame(3, PosProduct::where('owner_ref', $user->public_id)->count());
$coffee = PosProduct::where('owner_ref', $user->public_id)->where('name', 'Coffee')->firstOrFail();
$this->assertSame(1500, $coffee->price_minor);
$this->assertNotNull($coffee->category_id);
$this->assertFalse((bool) PosProduct::where('name', 'Croissant')->firstOrFail()->is_active);
// Drinks + Pastry created once each.
$this->assertSame(2, PosCategory::where('owner_ref', $user->public_id)->count());
}
public function test_retail_import_batches_to_crm_bulk_endpoint(): void
{
Http::fake([
'crm.test/api/products/bulk' => Http::response(['imported' => 2], 201),
'crm.test/api/*' => Http::response(['data' => []], 200),
]);
$user = $this->user(); // retail by default (no restaurant location)
$csv = "name,sku,price,description,active\n"
."Mug,MUG-01,25.00,Ceramic,yes\n"
."Tee,TSH-02,60.00,Cotton,yes\n";
$this->actingAs($user)
->post(route('pos.products.import'), ['file' => $this->csv($csv)])
->assertRedirect(route('pos.products.index'))
->assertSessionHas('success');
Http::assertSent(function ($request) {
return str_contains($request->url(), '/products/bulk')
&& count($request['products']) === 2
&& $request['products'][0]['name'] === 'Mug'
&& $request['products'][0]['unit_price_minor'] === 2500
&& $request['products'][0]['type'] === 'product';
});
// No local products created in retail mode.
$this->assertSame(0, PosProduct::where('owner_ref', $user->public_id)->count());
}
public function test_template_download_is_csv(): void
{
$response = $this->actingAs($this->user())
->get(route('pos.products.import.template'))
->assertOk()
->assertHeader('content-disposition', 'attachment; filename=pos-products-template.csv');
$this->assertStringContainsString('text/csv', $response->headers->get('content-type'));
$this->assertStringContainsString('name', $response->streamedContent());
}
}