diff --git a/fusion_clock/controllers/clock_nfc_kiosk.py b/fusion_clock/controllers/clock_nfc_kiosk.py index 0202d3e1..b67b4046 100644 --- a/fusion_clock/controllers/clock_nfc_kiosk.py +++ b/fusion_clock/controllers/clock_nfc_kiosk.py @@ -51,3 +51,56 @@ class FusionClockNfcKiosk(http.Controller): 'debug_enabled': ICP.get_param('fusion_clock.nfc_kiosk_debug', 'False') == 'True', } return request.render('fusion_clock.nfc_kiosk_page', values) + + @staticmethod + def _check_enroll_password(env, supplied): + """Verify the enroll-mode password. Empty config = always-allow for managers.""" + configured = env['ir.config_parameter'].sudo().get_param('fusion_clock.nfc_enroll_password', '') + if not configured: + return True + return (supplied or '') == configured + + @http.route('/fusion_clock/kiosk/nfc/enroll', type='jsonrpc', auth='user', methods=['POST']) + def nfc_enroll(self, employee_id=0, card_uid='', enroll_password='', **kw): + """Bind an NFC card UID to an employee. Manager-gated, password-gated.""" + user = request.env.user + if not user.has_group('fusion_clock.group_fusion_clock_manager'): + return {'error': 'access_denied'} + + if not self._check_enroll_password(request.env, enroll_password): + return {'error': 'invalid_password'} + + normalized = self._normalize_uid(card_uid) + if not normalized: + return {'error': 'invalid_uid'} + + Employee = request.env['hr.employee'].sudo() + target = Employee.browse(int(employee_id or 0)) + if not target.exists(): + return {'error': 'employee_not_found'} + + existing = Employee.search([ + ('x_fclk_nfc_card_uid', '=', normalized), + ('id', '!=', target.id), + ], limit=1) + if existing: + return { + 'error': 'card_already_assigned', + 'existing_employee': existing.name, + } + + target.x_fclk_nfc_card_uid = normalized + + # Activity log (uses 'card_enrollment' + 'nfc_kiosk' selections added in Task 2) + request.env['fusion.clock.activity.log'].sudo().create({ + 'employee_id': target.id, + 'log_type': 'card_enrollment', + 'description': f"NFC card {normalized} enrolled by {user.name}", + 'source': 'nfc_kiosk', + }) + + return { + 'success': True, + 'employee_name': target.name, + 'card_uid': normalized, + } diff --git a/fusion_clock/tests/test_clock_nfc_kiosk.py b/fusion_clock/tests/test_clock_nfc_kiosk.py index 03badb38..4f77ebf4 100644 --- a/fusion_clock/tests/test_clock_nfc_kiosk.py +++ b/fusion_clock/tests/test_clock_nfc_kiosk.py @@ -79,3 +79,75 @@ class TestUidNormalization(TransactionCase): def test_odd_length_returns_none(self): self.assertIsNone(FusionClockNfcKiosk._normalize_uid('04A2B562C18')) + + +import json + + +@tagged('-at_install', 'post_install', 'fusion_clock') +class TestEnrollEndpoint(HttpCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.ICP = cls.env['ir.config_parameter'].sudo() + cls.ICP.set_param('fusion_clock.enable_nfc_kiosk', 'True') + cls.ICP.set_param('fusion_clock.nfc_enroll_password', '1234') + cls.kiosk_user = cls.env['res.users'].create({ + 'name': 'Enroll Kiosk User', + 'login': 'nfc-kiosk-enroll', + 'password': 'kioskpass123', + 'group_ids': [(4, cls.env.ref('fusion_clock.group_fusion_clock_manager').id)], + }) + cls.alice = cls.env['hr.employee'].create({'name': 'Alice E', 'x_fclk_enable_clock': True}) + cls.bob = cls.env['hr.employee'].create({'name': 'Bob E', 'x_fclk_enable_clock': True}) + + def _call(self, payload): + self.authenticate('nfc-kiosk-enroll', 'kioskpass123') + response = self.url_open( + '/fusion_clock/kiosk/nfc/enroll', + data=json.dumps({'jsonrpc': '2.0', 'method': 'call', 'params': payload}), + headers={'Content-Type': 'application/json'}, + ) + return response.json().get('result', {}) + + def test_enroll_success(self): + result = self._call({ + 'employee_id': self.alice.id, + 'card_uid': '04:a2:b5:62:c1:80', + 'enroll_password': '1234', + }) + self.assertTrue(result.get('success')) + self.assertEqual(result.get('card_uid'), '04:A2:B5:62:C1:80') + self.alice.invalidate_recordset() + self.assertEqual(self.alice.x_fclk_nfc_card_uid, '04:A2:B5:62:C1:80') + + def test_enroll_wrong_password(self): + result = self._call({ + 'employee_id': self.alice.id, + 'card_uid': '04:A2:B5:62:C1:81', + 'enroll_password': 'wrong', + }) + self.assertEqual(result.get('error'), 'invalid_password') + self.alice.invalidate_recordset() + self.assertFalse(self.alice.x_fclk_nfc_card_uid) + + def test_enroll_card_already_assigned(self): + self.alice.x_fclk_nfc_card_uid = '04:A2:B5:62:C1:82' + result = self._call({ + 'employee_id': self.bob.id, + 'card_uid': '04:A2:B5:62:C1:82', + 'enroll_password': '1234', + }) + self.assertEqual(result.get('error'), 'card_already_assigned') + self.assertEqual(result.get('existing_employee'), 'Alice E') + self.bob.invalidate_recordset() + self.assertFalse(self.bob.x_fclk_nfc_card_uid) + + def test_enroll_invalid_uid(self): + result = self._call({ + 'employee_id': self.alice.id, + 'card_uid': 'not-a-uid', + 'enroll_password': '1234', + }) + self.assertEqual(result.get('error'), 'invalid_uid')