diff --git a/backend/docs/EQUIPMENT_STATUS.md b/backend/docs/EQUIPMENT_STATUS.md
new file mode 100644
index 0000000..9a9ef0f
--- /dev/null
+++ b/backend/docs/EQUIPMENT_STATUS.md
@@ -0,0 +1,149 @@
+# Equipment Status Mapping
+
+## Overview
+The `equipment_status` field tracks the state of IoT equipment for each beneficiary in the WellNuo system.
+
+## Database Field
+- **Table**: `beneficiaries`
+- **Column**: `equipment_status` (VARCHAR)
+- **Default**: `'none'`
+- **Constraint**: No CHECK constraint defined in schema (any string value allowed)
+
+## Valid Status Values
+
+| Status | Description | When Set | User Flow |
+|--------|-------------|----------|-----------|
+| `none` | No equipment ordered or assigned | • Initial beneficiary creation
• Default state | User sees "Purchase Equipment" screen |
+| `ordered` | Equipment ordered, payment completed | • After successful Stripe payment
• Order status: paid | User sees "Order Tracking" screen |
+| `shipped` | Equipment in transit | • Order status changes to shipped
• Tracking number assigned | User sees "Delivery Status" screen |
+| `delivered` | Equipment arrived at destination | • Order status changes to delivered | User sees "Activate Equipment" screen |
+| `demo` | Demo mode activated (no real devices) | • User activates with serial `DEMO-00000` or `DEMO-1234-5678` | Full app access with simulated data |
+| `active` | Real equipment activated and operational | • User activates with real device serial number | Full app access with real sensor data |
+
+## Code Locations
+
+### Setting Status
+```javascript
+// Initial creation (none)
+POST /api/me/beneficiaries
+ → equipment_status: 'none'
+
+// Activation (demo or active)
+POST /api/me/beneficiaries/:id/activate
+ → isDemoMode = serialNumber === 'DEMO-00000' || serialNumber === 'DEMO-1234-5678'
+ → equipment_status: isDemoMode ? 'demo' : 'active'
+
+// Order status changes (ordered, shipped, delivered)
+POST /api/webhook/stripe
+ → Updates based on Stripe events
+```
+
+### Reading Status
+```javascript
+// List beneficiaries
+GET /api/me/beneficiaries
+ → equipmentStatus: beneficiary.equipment_status || 'none'
+ → hasDevices: beneficiary.equipment_status === 'active' || beneficiary.equipment_status === 'demo'
+
+// Get single beneficiary
+GET /api/me/beneficiaries/:id
+ → equipmentStatus: beneficiary.equipment_status || 'none'
+ → hasDevices: beneficiary.equipment_status === 'active' || beneficiary.equipment_status === 'demo'
+```
+
+## Navigation Logic (NavigationController)
+
+The `equipmentStatus` determines which screen to show after login/beneficiary creation:
+
+```typescript
+if (equipmentStatus === 'none') {
+ → /(auth)/purchase - Buy equipment
+}
+
+if (equipmentStatus === 'ordered' || equipmentStatus === 'shipped') {
+ → /(tabs)/beneficiaries/:id/equipment - Track delivery
+}
+
+if (equipmentStatus === 'delivered') {
+ → /(auth)/activate - Activate equipment
+}
+
+if (equipmentStatus === 'active' || equipmentStatus === 'demo') {
+ → /(tabs)/dashboard - Full app access
+}
+```
+
+## hasDevices Flag
+
+**Critical for navigation:** `hasDevices` is derived from `equipmentStatus`:
+
+```javascript
+hasDevices = (equipmentStatus === 'active' || equipmentStatus === 'demo')
+```
+
+This flag is used in:
+- Navigation decisions (show dashboard vs purchase)
+- Feature access control (sensors, alarms, etc.)
+- UI conditional rendering
+
+## Issues Found
+
+1. **No Database Constraint**: The `equipment_status` column has no CHECK constraint, allowing any string value
+ - **Risk**: Typos, invalid values, inconsistent casing
+ - **Recommendation**: Add CHECK constraint in future migration
+
+2. **Frontend Handling**: Frontend code should handle all possible statuses gracefully
+ - Current implementation uses fallback: `equipmentStatus || 'none'`
+ - UI should handle unexpected values with default behavior
+
+3. **Order Status Sync**: Need to verify that order status changes properly update `equipment_status`
+ - Check Stripe webhook handlers
+ - Verify order table → beneficiaries sync
+
+## Testing Requirements
+
+### Unit Tests Needed
+- ✅ Activation sets correct status (demo vs active)
+- ✅ Beneficiary creation defaults to 'none'
+- ✅ hasDevices calculation is correct
+- ⚠️ Status transitions (none → ordered → shipped → delivered → active)
+- ⚠️ Invalid status handling
+
+### Integration Tests Needed
+- ⚠️ Order webhook → equipment_status update
+- ⚠️ Navigation flow for each status
+- ⚠️ UI rendering for each status
+
+## Recommendations
+
+1. **Add Database Constraint** (Future Migration):
+ ```sql
+ ALTER TABLE beneficiaries
+ ADD CONSTRAINT check_equipment_status
+ CHECK (equipment_status IN ('none', 'ordered', 'shipped', 'delivered', 'demo', 'active'));
+ ```
+
+2. **Create Constants File**:
+ ```javascript
+ // constants/equipmentStatus.js
+ const EQUIPMENT_STATUS = {
+ NONE: 'none',
+ ORDERED: 'ordered',
+ SHIPPED: 'shipped',
+ DELIVERED: 'delivered',
+ DEMO: 'demo',
+ ACTIVE: 'active'
+ };
+ ```
+
+3. **Add Status Validation** in API endpoints:
+ ```javascript
+ function isValidEquipmentStatus(status) {
+ return ['none', 'ordered', 'shipped', 'delivered', 'demo', 'active'].includes(status);
+ }
+ ```
+
+4. **Add Logging** for status changes:
+ ```javascript
+ console.log(`[EQUIPMENT_STATUS] ${beneficiaryId}: ${oldStatus} → ${newStatus}`);
+ ```
diff --git a/backend/src/routes/__tests__/beneficiaries-equipment-status.test.js b/backend/src/routes/__tests__/beneficiaries-equipment-status.test.js
new file mode 100644
index 0000000..6580678
--- /dev/null
+++ b/backend/src/routes/__tests__/beneficiaries-equipment-status.test.js
@@ -0,0 +1,451 @@
+const request = require('supertest');
+const express = require('express');
+const jwt = require('jsonwebtoken');
+const { supabase } = require('../../config/supabase');
+
+// Set JWT_SECRET for tests
+process.env.JWT_SECRET = 'test-secret';
+
+// Mock Stripe before requiring beneficiaries
+jest.mock('stripe', () => {
+ return jest.fn().mockImplementation(() => ({
+ checkout: {
+ sessions: {
+ create: jest.fn()
+ }
+ }
+ }));
+});
+
+// Mock supabase
+jest.mock('../../config/supabase', () => ({
+ supabase: {
+ from: jest.fn(),
+ auth: {
+ getUser: jest.fn()
+ }
+ }
+}));
+
+// Mock legacyAPI
+jest.mock('../../services/legacyAPI', () => ({
+ getDeploymentBeneficiaryName: jest.fn()
+}));
+
+// Mock storage
+jest.mock('../../services/storage', () => ({
+ uploadAvatar: jest.fn()
+}));
+
+const beneficiariesRouter = require('../beneficiaries');
+
+const app = express();
+app.use(express.json());
+app.use('/api/me/beneficiaries', beneficiariesRouter);
+
+// Helper to generate valid JWT token
+function generateToken(userId = 1, email = 'test@example.com') {
+ return jwt.sign({ userId, email }, process.env.JWT_SECRET);
+}
+
+describe('Equipment Status Mapping', () => {
+ let validToken;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ validToken = generateToken();
+ });
+
+ describe('GET /api/me/beneficiaries', () => {
+ it('should map equipment_status correctly for "none"', async () => {
+ const mockBeneficiary = {
+ id: 1,
+ name: 'Test Beneficiary',
+ equipment_status: 'none',
+ phone: null,
+ address: null,
+ avatar_url: null,
+ created_at: '2025-01-01T00:00:00Z',
+ subscription_status: null
+ };
+
+ const mockAccess = {
+ beneficiary_id: 1,
+ accessor_id: 1,
+ role: 'custodian',
+ granted_at: '2025-01-01T00:00:00Z',
+ custom_name: null
+ };
+
+ supabase.from.mockImplementation((table) => {
+ if (table === 'user_access') {
+ return {
+ select: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockResolvedValue({ data: [mockAccess], error: null })
+ };
+ }
+ if (table === 'beneficiaries') {
+ return {
+ select: jest.fn().mockReturnThis(),
+ in: jest.fn().mockResolvedValue({ data: [mockBeneficiary], error: null })
+ };
+ }
+ return {
+ select: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockResolvedValue({ data: null, error: null })
+ };
+ });
+
+ const response = await request(app)
+ .get('/api/me/beneficiaries')
+ .set('Authorization', `Bearer ${validToken}`);
+
+ expect(response.status).toBe(200);
+ expect(response.body.beneficiaries).toHaveLength(1);
+ expect(response.body.beneficiaries[0]).toMatchObject({
+ equipmentStatus: 'none',
+ hasDevices: false
+ });
+ });
+
+ it('should map equipment_status correctly for "demo"', async () => {
+ const mockBeneficiary = {
+ id: 1,
+ name: 'Test Beneficiary',
+ equipment_status: 'demo',
+ phone: null,
+ address: null,
+ avatar_url: null,
+ created_at: '2025-01-01T00:00:00Z',
+ subscription_status: null
+ };
+
+ const mockAccess = {
+ beneficiary_id: 1,
+ accessor_id: 1,
+ role: 'custodian',
+ granted_at: '2025-01-01T00:00:00Z',
+ custom_name: null
+ };
+
+ supabase.from.mockImplementation((table) => {
+ if (table === 'user_access') {
+ return {
+ select: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockResolvedValue({ data: [mockAccess], error: null })
+ };
+ }
+ if (table === 'beneficiaries') {
+ return {
+ select: jest.fn().mockReturnThis(),
+ in: jest.fn().mockResolvedValue({ data: [mockBeneficiary], error: null })
+ };
+ }
+ return {
+ select: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockResolvedValue({ data: null, error: null })
+ };
+ });
+
+ const response = await request(app)
+ .get('/api/me/beneficiaries')
+ .set('Authorization', `Bearer ${validToken}`);
+
+ expect(response.status).toBe(200);
+ expect(response.body.beneficiaries).toHaveLength(1);
+ expect(response.body.beneficiaries[0]).toMatchObject({
+ equipmentStatus: 'demo',
+ hasDevices: true
+ });
+ });
+
+ it('should map equipment_status correctly for "active"', async () => {
+ const mockBeneficiary = {
+ id: 1,
+ name: 'Test Beneficiary',
+ equipment_status: 'active',
+ phone: null,
+ address: null,
+ avatar_url: null,
+ created_at: '2025-01-01T00:00:00Z',
+ subscription_status: null
+ };
+
+ const mockAccess = {
+ beneficiary_id: 1,
+ accessor_id: 1,
+ role: 'custodian',
+ granted_at: '2025-01-01T00:00:00Z',
+ custom_name: null
+ };
+
+ supabase.from.mockImplementation((table) => {
+ if (table === 'user_access') {
+ return {
+ select: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockResolvedValue({ data: [mockAccess], error: null })
+ };
+ }
+ if (table === 'beneficiaries') {
+ return {
+ select: jest.fn().mockReturnThis(),
+ in: jest.fn().mockResolvedValue({ data: [mockBeneficiary], error: null })
+ };
+ }
+ return {
+ select: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockResolvedValue({ data: null, error: null })
+ };
+ });
+
+ const response = await request(app)
+ .get('/api/me/beneficiaries')
+ .set('Authorization', `Bearer ${validToken}`);
+
+ expect(response.status).toBe(200);
+ expect(response.body.beneficiaries).toHaveLength(1);
+ expect(response.body.beneficiaries[0]).toMatchObject({
+ equipmentStatus: 'active',
+ hasDevices: true
+ });
+ });
+
+ it('should default to "none" when equipment_status is null', async () => {
+ const mockBeneficiary = {
+ id: 1,
+ name: 'Test Beneficiary',
+ equipment_status: null,
+ phone: null,
+ address: null,
+ avatar_url: null,
+ created_at: '2025-01-01T00:00:00Z',
+ subscription_status: null
+ };
+
+ const mockAccess = {
+ beneficiary_id: 1,
+ accessor_id: 1,
+ role: 'custodian',
+ granted_at: '2025-01-01T00:00:00Z',
+ custom_name: null
+ };
+
+ supabase.from.mockImplementation((table) => {
+ if (table === 'user_access') {
+ return {
+ select: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockResolvedValue({ data: [mockAccess], error: null })
+ };
+ }
+ if (table === 'beneficiaries') {
+ return {
+ select: jest.fn().mockReturnThis(),
+ in: jest.fn().mockResolvedValue({ data: [mockBeneficiary], error: null })
+ };
+ }
+ return {
+ select: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockResolvedValue({ data: null, error: null })
+ };
+ });
+
+ const response = await request(app)
+ .get('/api/me/beneficiaries')
+ .set('Authorization', `Bearer ${validToken}`);
+
+ expect(response.status).toBe(200);
+ expect(response.body.beneficiaries).toHaveLength(1);
+ expect(response.body.beneficiaries[0]).toMatchObject({
+ equipmentStatus: 'none',
+ hasDevices: false
+ });
+ });
+
+ it('should handle order statuses (ordered, shipped, delivered)', async () => {
+ const statuses = ['ordered', 'shipped', 'delivered'];
+
+ for (const status of statuses) {
+ const mockBeneficiary = {
+ id: 1,
+ name: 'Test Beneficiary',
+ equipment_status: status,
+ phone: null,
+ address: null,
+ avatar_url: null,
+ created_at: '2025-01-01T00:00:00Z',
+ subscription_status: null
+ };
+
+ const mockAccess = {
+ beneficiary_id: 1,
+ accessor_id: 1,
+ role: 'custodian',
+ granted_at: '2025-01-01T00:00:00Z',
+ custom_name: null
+ };
+
+ supabase.from.mockImplementation((table) => {
+ if (table === 'user_access') {
+ return {
+ select: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockResolvedValue({ data: [mockAccess], error: null })
+ };
+ }
+ if (table === 'beneficiaries') {
+ return {
+ select: jest.fn().mockReturnThis(),
+ in: jest.fn().mockResolvedValue({ data: [mockBeneficiary], error: null })
+ };
+ }
+ return {
+ select: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockResolvedValue({ data: null, error: null })
+ };
+ });
+
+ const response = await request(app)
+ .get('/api/me/beneficiaries')
+ .set('Authorization', `Bearer ${validToken}`);
+
+ expect(response.status).toBe(200);
+ expect(response.body.beneficiaries).toHaveLength(1);
+ expect(response.body.beneficiaries[0]).toMatchObject({
+ equipmentStatus: status,
+ hasDevices: false // Order statuses don't grant device access
+ });
+ }
+ });
+ });
+
+ describe('POST /api/me/beneficiaries/:id/activate', () => {
+ it('should set equipment_status to "demo" for demo serial numbers', async () => {
+ const demoSerials = ['DEMO-00000', 'DEMO-1234-5678'];
+
+ for (const serialNumber of demoSerials) {
+ const mockAccess = {
+ beneficiary_id: 1,
+ accessor_id: 1,
+ role: 'custodian'
+ };
+
+ const mockBeneficiary = {
+ id: 1,
+ name: 'Test Beneficiary',
+ equipment_status: 'demo'
+ };
+
+ supabase.from.mockImplementation((table) => {
+ if (table === 'user_access') {
+ return {
+ select: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockReturnThis(),
+ single: jest.fn().mockResolvedValue({ data: mockAccess, error: null })
+ };
+ }
+ if (table === 'beneficiaries') {
+ return {
+ update: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockReturnThis(),
+ select: jest.fn().mockReturnThis(),
+ single: jest.fn().mockResolvedValue({ data: mockBeneficiary, error: null })
+ };
+ }
+ return {
+ select: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockResolvedValue({ data: null, error: null })
+ };
+ });
+
+ const response = await request(app)
+ .post('/api/me/beneficiaries/1/activate')
+ .set('Authorization', `Bearer ${validToken}`)
+ .send({ serialNumber });
+
+ expect(response.status).toBe(200);
+ expect(response.body.beneficiary).toMatchObject({
+ equipmentStatus: 'demo',
+ hasDevices: true
+ });
+ }
+ });
+
+ it('should set equipment_status to "active" for real serial numbers', async () => {
+ const mockAccess = {
+ beneficiary_id: 1,
+ accessor_id: 1,
+ role: 'custodian'
+ };
+
+ const mockBeneficiary = {
+ id: 1,
+ name: 'Test Beneficiary',
+ equipment_status: 'active'
+ };
+
+ supabase.from.mockImplementation((table) => {
+ if (table === 'user_access') {
+ return {
+ select: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockReturnThis(),
+ single: jest.fn().mockResolvedValue({ data: mockAccess, error: null })
+ };
+ }
+ if (table === 'beneficiaries') {
+ return {
+ update: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockReturnThis(),
+ select: jest.fn().mockReturnThis(),
+ single: jest.fn().mockResolvedValue({ data: mockBeneficiary, error: null })
+ };
+ }
+ return {
+ select: jest.fn().mockReturnThis(),
+ eq: jest.fn().mockResolvedValue({ data: null, error: null })
+ };
+ });
+
+ const response = await request(app)
+ .post('/api/me/beneficiaries/1/activate')
+ .set('Authorization', `Bearer ${validToken}`)
+ .send({ serialNumber: 'SN-12345678' });
+
+ expect(response.status).toBe(200);
+ expect(response.body.beneficiary).toMatchObject({
+ equipmentStatus: 'active',
+ hasDevices: true
+ });
+ });
+ });
+
+ describe('hasDevices calculation', () => {
+ it('should return true for "active" status', () => {
+ const hasDevices = 'active' === 'active' || 'active' === 'demo';
+ expect(hasDevices).toBe(true);
+ });
+
+ it('should return true for "demo" status', () => {
+ const hasDevices = 'demo' === 'active' || 'demo' === 'demo';
+ expect(hasDevices).toBe(true);
+ });
+
+ it('should return false for "none" status', () => {
+ const hasDevices = 'none' === 'active' || 'none' === 'demo';
+ expect(hasDevices).toBe(false);
+ });
+
+ it('should return false for "ordered" status', () => {
+ const hasDevices = 'ordered' === 'active' || 'ordered' === 'demo';
+ expect(hasDevices).toBe(false);
+ });
+
+ it('should return false for "shipped" status', () => {
+ const hasDevices = 'shipped' === 'active' || 'shipped' === 'demo';
+ expect(hasDevices).toBe(false);
+ });
+
+ it('should return false for "delivered" status', () => {
+ const hasDevices = 'delivered' === 'active' || 'delivered' === 'demo';
+ expect(hasDevices).toBe(false);
+ });
+ });
+});