createUserSession(['user']); api_fixtures()->addCustomerAttribute((int)$session['user']['id'], 'onlyTankCleaning'); $response = api_client()->get( '/customer/attributes?customer_number=' . (int)$session['user']['customer_number'], $session['headers'] ); $response ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); $attributes = array_map( static fn(array $attribute): string => (string)($attribute['attribute'] ?? ''), is_array($response->data()) ? $response->data() : [] ); expect($attributes)->toContain('onlyTankCleaning'); expect($response->body)->not->toContain('list_customer_attributes'); }); it('keeps customer attribute reads scoped to the authenticated customer', function (): void { api_test_covers('GET /customer/attributes', 'customer-access'); $session = api_fixtures()->createUserSession(['user']); $otherCustomer = api_fixtures()->createUser(['display_name' => 'Other Attribute Customer']); api_fixtures()->addCustomerAttribute((int)$otherCustomer['id'], 'onlyTankCleaning'); $response = api_client()->get( '/customer/attributes?customer_number=' . (int)$otherCustomer['customer_number'], $session['headers'] ); $response ->assertStatus(403) ->assertEnvelope() ->assertSuccess(false) ->assertMissingPermissions(['list_customer_attributes']); }); it('still lets attribute managers read another customer attributes', function (): void { api_test_covers('GET /customer/attributes', 'permissions'); $session = api_fixtures()->createUserSession(['list_customer_attributes']); $customer = api_fixtures()->createUser(['display_name' => 'Managed Attribute Customer']); api_fixtures()->addCustomerAttribute((int)$customer['id'], 'onlyTankCleaning'); $response = api_client()->get( '/customer/attributes?customer_number=' . (int)$customer['customer_number'], $session['headers'] ); $response ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); $attributes = array_map( static fn(array $attribute): string => (string)($attribute['attribute'] ?? ''), is_array($response->data()) ? $response->data() : [] ); expect($attributes)->toContain('onlyTankCleaning'); }); it('returns exact product restrictions for product-impact attributes and null for workflow attributes', function (): void { api_test_covers('GET /customer/attributes', 'product_restrictions'); $session = api_fixtures()->createUserSession(['list_customer_attributes']); $customer = api_fixtures()->createUser(['display_name' => 'Configured Attribute Customer']); api_fixtures()->addCustomerAttribute((int)$customer['id'], 'restrictSpotFree'); api_fixtures()->addCustomerAttribute((int)$customer['id'], 'exemptFromAdministrationFee'); $product = api_fixtures()->createProduct(['name' => 'Configured exact rinse']); new \classes\customer_rule_product_restriction_service(); $db = api_test_runtime()->db(); $collectionResult = $db->query( "SELECT id FROM customer_rule_product_collections WHERE attribute = 'restrictSpotFree' ORDER BY sort_order, id LIMIT 1" ); $collectionId = (int)$collectionResult->fetch_assoc()['id']; $db->query( "INSERT IGNORE INTO customer_rule_product_collection_products (collection_id, product_id) VALUES ({$collectionId}, " . (int)$product['id'] . ')' ); api_fixtures()->cleanupDeleteWhere('customer_rule_product_collection_products', [ 'collection_id' => $collectionId, 'product_id' => (int)$product['id'], ]); $response = api_client()->get( '/customer/attributes?customer_number=' . (int)$customer['customer_number'], $session['headers'] ); $response->assertStatus(200)->assertEnvelope()->assertSuccess(); $byAttribute = []; foreach ($response->data() as $attribute) { $byAttribute[(string)$attribute['attribute']] = $attribute; } expect($byAttribute['restrictSpotFree']['product_restriction']['disabled_product_ids'] ?? []) ->toContain((int)$product['id']) ->and($byAttribute['restrictSpotFree']['product_restriction']['collections'] ?? [])->not->toBeEmpty() ->and($byAttribute['exemptFromAdministrationFee'])->toHaveKey('product_restriction') ->and($byAttribute['exemptFromAdministrationFee']['product_restriction'])->toBeNull(); }); it('keeps workflow-only customer attribute activation compatible', function (): void { api_test_covers('POST /customer/attributes', 'workflow_compatibility'); api_test_covers('DELETE /customer/attributes', 'workflow_compatibility'); $session = api_fixtures()->createUserSession([ 'list_customer_attributes', 'add_customer_attribute', 'delete_customer_attribute', ]); $customer = api_fixtures()->createUser(['display_name' => 'Workflow Attribute Customer']); api_client()->post('/customer/attributes', [ 'user_id' => (int)$customer['id'], 'attribute' => 'exemptFromAdministrationFee', ], $session['headers']) ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); $listed = api_client()->get( '/customer/attributes?customer_number=' . (int)$customer['customer_number'], $session['headers'] ); expect(array_column($listed->data(), 'attribute'))->toContain('exemptFromAdministrationFee'); api_client()->delete( '/customer/attributes?user_id=' . (int)$customer['id'] . '&attribute=exemptFromAdministrationFee', null, $session['headers'] ) ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); });