'owner-loyalty', 'name' => 'Owner', 'email' => 'owner-loyalty@example.com', ]); } protected function setUp(): void { parent::setUp(); $this->withoutMiddleware(EnsurePlatformSession::class); $this->withoutVite(); config(['crm.url' => 'https://crm.test/api', 'crm.key' => 'pos-key']); } public function test_create_sale_redeems_points_via_crm_and_reduces_total(): void { Http::fake([ 'crm.test/api/loyalty/redeem*' => Http::response([ 'points' => 200, 'discount_minor' => 200, 'ledger_id' => 9, 'balance' => 50, ], 200), 'crm.test/api/loyalty/earn*' => Http::response([ 'points' => 8, 'ledger_id' => 10, 'balance' => 58, ], 200), ]); $owner = $this->owner(); PosLocation::create([ 'owner_ref' => $owner->public_id, 'name' => 'Main', 'currency' => 'GHS', 'is_default' => true, ]); $sales = app(PosSaleService::class); $sale = $sales->createSale($owner, [ ['name' => 'Shirt', 'unit_price_minor' => 1000, 'quantity' => 1], ], [ 'crm_customer_id' => 42, 'loyalty_redeem_points' => 200, 'currency' => 'GHS', ]); $this->assertSame(1000, $sale->subtotal_minor); $this->assertSame(200, $sale->loyalty_discount_minor); $this->assertSame(200, $sale->loyalty_points_redeemed); $this->assertSame(800, $sale->total_minor); $paid = $sales->recordCashPayment($sale); $this->assertSame(8, $paid->loyalty_points_earned); Http::assertSent(function ($request) { return str_contains($request->url(), 'loyalty/redeem') && $request['customer_id'] === 42 && $request['redeem_points'] === 200; }); Http::assertSent(function ($request) { return str_contains($request->url(), 'loyalty/earn') && $request['amount_minor'] === 800; }); } public function test_cancel_pending_sale_reverses_loyalty(): void { Http::fake([ 'crm.test/api/loyalty/redeem*' => Http::response([ 'points' => 100, 'discount_minor' => 100, 'ledger_id' => 1, 'balance' => 0, ], 200), 'crm.test/api/loyalty/reverse*' => Http::response(['reversed' => 1], 200), ]); $owner = $this->owner(); $sales = app(PosSaleService::class); $sale = $sales->createSale($owner, [ ['name' => 'Item', 'unit_price_minor' => 500, 'quantity' => 1], ], [ 'crm_customer_id' => 7, 'loyalty_redeem_points' => 100, ]); $sales->cancelSale($sale); $this->assertSame(PosSale::STATUS_CANCELLED, $sale->fresh()->status); Http::assertSent(fn ($request) => str_contains($request->url(), 'loyalty/reverse')); } public function test_register_loyalty_endpoint_returns_quote(): void { Http::fake([ 'crm.test/api/customers/5/loyalty*' => Http::response([ 'program' => ['enabled' => true, 'name' => 'Rewards'], 'label' => 'Rewards', 'points_balance' => 300, 'points_earn' => 10, 'max_redeem_points' => 200, ], 200), 'crm.test/api/loyalty/quote*' => Http::response([ 'points_balance' => 300, 'points' => 100, 'discount_minor' => 100, 'max_points' => 200, 'points_earn' => 9, 'payable_minor' => 900, ], 200), ]); $owner = $this->owner(); PosLocation::create([ 'owner_ref' => $owner->public_id, 'name' => 'Main', 'currency' => 'GHS', 'is_default' => true, ]); $this->actingAs($owner) ->getJson(route('pos.register.loyalty', [ 'crm_customer_id' => 5, 'subtotal_minor' => 1000, 'redeem_points' => 100, ])) ->assertOk() ->assertJsonPath('enabled', true) ->assertJsonPath('quote.discount_minor', 100) ->assertJsonPath('snapshot.points_balance', 300); } public function test_display_payload_maps_snapshot(): void { $payload = app(PosLoyaltyService::class)->displayPayload([ 'program' => ['enabled' => true, 'name' => 'Rewards'], 'label' => 'Rewards', 'points_balance' => 40, 'points_earn' => 5, ], 10, 5); $this->assertSame(40, $payload['points_balance']); $this->assertSame(10, $payload['points_redeem']); $this->assertSame(5, $payload['points_earn']); } }