Deploy Ladill POS / deploy (push) Successful in 1m39s
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>
107 lines
4.0 KiB
PHP
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());
|
|
}
|
|
}
|