Email digital receipts, add promo settings, and apply tips at charge.
Deploy Ladill POS / deploy (push) Successful in 31s

Send real receipt emails from the customer display and sale page, store
branch promo content for the idle screen, and fold selected tips into totals.
This commit is contained in:
isaacclad
2026-07-15 16:10:20 +00:00
parent 468346b183
commit 600aedb59d
20 changed files with 675 additions and 19 deletions
+185
View File
@@ -0,0 +1,185 @@
<?php
namespace Tests\Feature;
use App\Http\Middleware\EnsurePlatformSession;
use App\Mail\DigitalReceiptMail;
use App\Models\PosCustomerDisplay;
use App\Models\PosLocation;
use App\Models\PosSale;
use App\Models\User;
use App\Services\Pos\CustomerDisplayService;
use App\Services\Pos\PosSaleService;
use App\Services\Pos\ReceiptEmailService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class ReceiptTipPromoTest extends TestCase
{
use RefreshDatabase;
private function owner(): User
{
return User::create([
'public_id' => 'owner-rtp',
'name' => 'Owner',
'email' => 'owner-rtp@example.com',
]);
}
private function location(User $owner): PosLocation
{
return PosLocation::create([
'owner_ref' => $owner->public_id,
'name' => 'Front counter',
'currency' => 'GHS',
'is_default' => true,
]);
}
protected function setUp(): void
{
parent::setUp();
$this->withoutMiddleware(EnsurePlatformSession::class);
$this->withoutVite();
}
public function test_tip_is_applied_to_sale_total_on_create(): void
{
$owner = $this->owner();
$this->location($owner);
$sale = app(PosSaleService::class)->createSale($owner, [
['name' => 'Meal', 'unit_price_minor' => 10000, 'quantity' => 1],
], [
'tip_minor' => 1500,
'currency' => 'GHS',
]);
$this->assertSame(10000, $sale->subtotal_minor);
$this->assertSame(1500, $sale->tip_minor);
$this->assertSame(11500, $sale->total_minor);
}
public function test_digital_receipt_email_is_sent(): void
{
Mail::fake();
$owner = $this->owner();
$location = $this->location($owner);
$sale = PosSale::create([
'owner_ref' => $owner->public_id,
'location_id' => $location->id,
'reference' => 'POS-TESTRECEIPT1',
'status' => PosSale::STATUS_PAID,
'payment_method' => PosSale::METHOD_CASH,
'subtotal_minor' => 500,
'total_minor' => 500,
'currency' => 'GHS',
'paid_at' => now(),
]);
$result = app(ReceiptEmailService::class)->send($sale, 'guest@example.com');
$this->assertTrue($result['ok']);
Mail::assertSent(DigitalReceiptMail::class, function (DigitalReceiptMail $mail) use ($sale) {
return $mail->sale->is($sale)
&& $mail->hasTo('guest@example.com');
});
$this->assertSame('guest@example.com', $sale->fresh()->customer_email);
}
public function test_customer_display_receipt_email_action_sends_mail(): void
{
Mail::fake();
$owner = $this->owner();
$location = $this->location($owner);
$sale = PosSale::create([
'owner_ref' => $owner->public_id,
'location_id' => $location->id,
'reference' => 'POS-TESTRECEIPT2',
'status' => PosSale::STATUS_PAID,
'payment_method' => PosSale::METHOD_CASH,
'subtotal_minor' => 800,
'total_minor' => 800,
'currency' => 'GHS',
'paid_at' => now(),
]);
$display = app(CustomerDisplayService::class)->pushReceipt($location, $sale);
$this->postJson(route('pos.customer-display.action', ['token' => $display->token]), [
'type' => 'receipt_email',
'email' => 'buyer@example.com',
])
->assertOk()
->assertJsonPath('email_sent', true);
Mail::assertSent(DigitalReceiptMail::class);
}
public function test_settings_save_customer_promo_content(): void
{
Storage::fake('public');
$owner = $this->owner();
$this->location($owner);
$this->actingAs($owner)
->put(route('pos.settings.update'), [
'name' => 'Front counter',
'currency' => 'GHS',
'service_style' => 'retail',
'receipt_footer' => '',
'receipt_header' => 'Demo Shop',
'printer_paper_mm' => 80,
'customer_promo_headline' => 'Happy hour',
'customer_promo_body' => 'Buy one get one free until 6pm.',
'customer_promo_image' => UploadedFile::fake()->image('promo.jpg'),
'gateway_provider' => '',
])
->assertRedirect();
$location = PosLocation::query()->first();
$this->assertSame('Happy hour', $location->customer_promo_headline);
$this->assertSame('Buy one get one free until 6pm.', $location->customer_promo_body);
$this->assertNotNull($location->customer_promo_image_path);
$promo = $location->customerPromo();
$this->assertSame('Happy hour', $promo['headline']);
$this->assertNotNull($promo['image_url']);
$display = app(CustomerDisplayService::class)->ensureForLocation($location);
$state = app(CustomerDisplayService::class)->publicState($display);
$this->assertSame('Happy hour', $state['promo']['headline']);
}
public function test_operator_can_email_receipt_from_sale_page(): void
{
Mail::fake();
$owner = $this->owner();
$location = $this->location($owner);
$sale = PosSale::create([
'owner_ref' => $owner->public_id,
'location_id' => $location->id,
'reference' => 'POS-TESTRECEIPT3',
'status' => PosSale::STATUS_PAID,
'payment_method' => PosSale::METHOD_CASH,
'subtotal_minor' => 300,
'total_minor' => 300,
'currency' => 'GHS',
'paid_at' => now(),
]);
$this->actingAs($owner)
->post(route('pos.sales.email-receipt', $sale), ['email' => 'print@example.com'])
->assertRedirect();
Mail::assertSent(DigitalReceiptMail::class);
}
}