# @feedmepos/hrm-permission

Permission types, enums, and access checking for FeedMe services.

## Installation

```bash
pnpm add @feedmepos/hrm-permission
```

---

## Usage

### Permission Enums

All subjects are in `PermissionSubjectBusinessNamespace`, exposed via `Permission.Subject.Business`.

```typescript
import {
	Permission,
	PermissionAction,
	PermissionSubjectBusinessNamespace,
} from '@feedmepos/hrm-permission';

// Actions
PermissionAction.manage; // full control
PermissionAction.read;
PermissionAction.create;
PermissionAction.update;
PermissionAction.delete;

// Subjects — use either form, they are the same value
Permission.Subject.Business.hrm_teamMember; // 'business::hrm::teamMember'
PermissionSubjectBusinessNamespace.hrm_teamMember; // same

// Common subjects by module
Permission.Subject.Business.menu_item; // 'business::menu::item'
Permission.Subject.Business.menu_catalog; // 'business::menu::catalog'
Permission.Subject.Business.menu_menuManagement; // 'business::menu::menuManagement'
// ... (see full menu list in the collapsible below)
Permission.Subject.Business.restaurant; // 'business::restaurant'

Permission.Subject.Business.crm_promotion; // 'business::crm::promotion'
Permission.Subject.Business.crm_voucher; // 'business::crm::voucher'
Permission.Subject.Business.crm_membership; // 'business::crm::membership'

Permission.Subject.Business.payment_payoutAccount; // 'business::payment::payoutAccount'
Permission.Subject.Business.payment_paymentOnboarding; // 'business::payment::paymentOnboarding'
Permission.Subject.Business.payment_transactions; // 'business::payment::transactions'
Permission.Subject.Business.payment_settlements; // 'business::payment::settlements'

Permission.Subject.Business.inventory_stockBalance; // 'business::inventory::stockBalance'
Permission.Subject.Business.inventory_ingredient; // 'business::inventory::ingredient'
Permission.Subject.Business.inventory_recipe; // 'business::inventory::recipe'
// ... (see full list in the collapsible below)

Permission.Subject.Business.hrm_employee; // 'business::hrm::employee'
Permission.Subject.Business.hrm_teamMember; // 'business::hrm::teamMember'
Permission.Subject.Business.hrm_auditLog; // 'business::hrm::auditLog'

Permission.Subject.Business.report_createReport; // 'business::report::createReport'
Permission.Subject.Business.report_accessOverview; // 'business::report::accessOverview'
```

<details>
<summary>Full subject reference</summary>

```typescript
// General
Permission.Subject.Business.profile;
Permission.Subject.Business.restaurant;

// Menu
Permission.Subject.Business.menu_item; // 'business::menu::item'
Permission.Subject.Business.menu_catalog; // 'business::menu::catalog'
Permission.Subject.Business.menu_category; // 'business::menu::category'
Permission.Subject.Business.menu_subCategory; // 'business::menu::subCategory'
Permission.Subject.Business.menu_group; // 'business::menu::group'
Permission.Subject.Business.menu_takeaway; // 'business::menu::takeaway'
Permission.Subject.Business.menu_scheduler; // 'business::menu::scheduler'
Permission.Subject.Business.menu_variant; // 'business::menu::variant'
Permission.Subject.Business.menu_cookingGuide; // 'business::menu::cookingGuide'
Permission.Subject.Business.menu_printRoute; // 'business::menu::printRoute'
Permission.Subject.Business.menu_servingSequence; // 'business::menu::servingSequence'
Permission.Subject.Business.menu_unit; // 'business::menu::unit'
Permission.Subject.Business.menu_ingredient; // 'business::menu::ingredient'
Permission.Subject.Business.menu_recipe; // 'business::menu::recipe'
Permission.Subject.Business.menu_settings; // 'business::menu::settings'
Permission.Subject.Business.menu_publish; // 'business::menu::publish'
Permission.Subject.Business.menu_menuManagement; // 'business::menu::menuManagement'
Permission.Subject.Business.menu_importExport; // 'business::menu::importExport'

// CRM
Permission.Subject.Business.crm_promotion; // 'business::crm::promotion'
Permission.Subject.Business.crm_voucher; // 'business::crm::voucher'
Permission.Subject.Business.crm_membership; // 'business::crm::membership'
Permission.Subject.Business.crm_analytic; // 'business::crm::analytic'
Permission.Subject.Business.crm_tier; // 'business::crm::tier'
Permission.Subject.Business.crm_title; // 'business::crm::title'
Permission.Subject.Business.crm_broadcast; // 'business::crm::broadcast'
Permission.Subject.Business.crm_point; // 'business::crm::point'
Permission.Subject.Business.crm_credit; // 'business::crm::credit'
Permission.Subject.Business.crm_experience; // 'business::crm::experience'
Permission.Subject.Business.crm_game; // 'business::crm::game'
Permission.Subject.Business.crm_mission; // 'business::crm::mission'
Permission.Subject.Business.crm_loyaltyMember; // 'business::crm::loyaltyMember'
Permission.Subject.Business.crm_loyaltySegment; // 'business::crm::loyaltySegment'
Permission.Subject.Business.crm_loyaltyCard; // 'business::crm::loyaltyCard'
Permission.Subject.Business.crm_referral; // 'business::crm::referral'
Permission.Subject.Business.crm_store; // 'business::crm::store'
Permission.Subject.Business.crm_transaction; // 'business::crm::transaction'
Permission.Subject.Business.crm_setting; // 'business::crm::setting'
Permission.Subject.Business.crm_bin; // 'business::crm::bin'

// Payment
Permission.Subject.Business.payment_payoutAccount; // 'business::payment::payoutAccount'
Permission.Subject.Business.payment_paymentOnboarding; // 'business::payment::paymentOnboarding'
Permission.Subject.Business.payment_transactions; // 'business::payment::transactions'
Permission.Subject.Business.payment_settlements; // 'business::payment::settlements'

// Inventory
Permission.Subject.Business.inventory_stock; // 'business::inventory::stock'
Permission.Subject.Business.inventory_stockBalance; // 'business::inventory::stockBalance'
Permission.Subject.Business.inventory_stockAdjustment; // 'business::inventory::stockAdjustment'
Permission.Subject.Business.inventory_unitCostHistory; // 'business::inventory::unitCostHistory'
Permission.Subject.Business.inventory_wastageTemplate; // 'business::inventory::wastageTemplate'
Permission.Subject.Business.inventory_closingHistory; // 'business::inventory::closingHistory'
Permission.Subject.Business.inventory_closingTemplate; // 'business::inventory::closingTemplate'
Permission.Subject.Business.inventory_closingDraft; // 'business::inventory::closingDraft'
Permission.Subject.Business.inventory_ingredient; // 'business::inventory::ingredient'
Permission.Subject.Business.inventory_ingredientGroup; // 'business::inventory::ingredientGroup'
Permission.Subject.Business.inventory_recipe; // 'business::inventory::recipe'
Permission.Subject.Business.inventory_unit; // 'business::inventory::unit'
Permission.Subject.Business.inventory_purchaseTransfer; // 'business::inventory::purchaseTransfer'
Permission.Subject.Business.inventory_orderDraftApproval; // 'business::inventory::orderDraftApproval'
Permission.Subject.Business.inventory_transferOut; // 'business::inventory::transferOut'
Permission.Subject.Business.inventory_surcharge; // 'business::inventory::surcharge'
Permission.Subject.Business.inventory_orderTemplate; // 'business::inventory::orderTemplate'
Permission.Subject.Business.inventory_supplier; // 'business::inventory::supplier'
Permission.Subject.Business.inventory_warehouse; // 'business::inventory::warehouse'
Permission.Subject.Business.inventory_publish; // 'business::inventory::publish'
Permission.Subject.Business.inventory_import; // 'business::inventory::import'
Permission.Subject.Business.inventory_integration; // 'business::inventory::integration'

// HRM
Permission.Subject.Business.hrm_employee; // 'business::hrm::employee'
Permission.Subject.Business.hrm_teamMember; // 'business::hrm::teamMember'
Permission.Subject.Business.hrm_auditLog; // 'business::hrm::auditLog'

// Report
Permission.Subject.Business.report_createReport; // 'business::report::createReport'
Permission.Subject.Business.report_accessOverview; // 'business::report::accessOverview'
Permission.Subject.Business.report_accessInsight; // 'business::report::accessInsight'
Permission.Subject.Business.report_accessSetting; // 'business::report::accessSetting'
Permission.Subject.Business.report_accessIntegration; // 'business::report::accessIntegration'
Permission.Subject.Business.report_reports_allDefaultReports; // 'business::report::allDefaultReports'
Permission.Subject.Business.report_reports_allCustomReports; // 'business::report::allCustomReports'
```

</details>

### checkAccess

```typescript
import { checkAccess } from '@feedmepos/hrm-permission';
import type { RawRule } from '@casl/ability';

// userPermissions comes from your permission service / store
const userPermissions: RawRule[] = /* ... */;
```

### Basic check (AND — all must pass)

```typescript
const result = checkAccess(
	[
		{ action: PermissionAction.manage, subject: Permission.Subject.Business.hrm_teamMember },
		{ action: PermissionAction.read, subject: Permission.Subject.Business.hrm_employee },
	],
	userPermissions
);

if (result.granted) {
	// allowed
} else {
	console.log('blocked by', result.decisivePermission);
}
```

### coverSubject — OR fallback

Grants access when the user can perform the action on **either** `subject` **or** `coverSubject`. Useful for "group covers individual" patterns (e.g. a catch-all report permission covering a specific dynamic report subject).

```typescript
const result = checkAccess(
	[
		{
			action: PermissionAction.read,
			subject: 'business::report::myDynamicReport',
			coverSubject: Permission.Subject.Business.report_reports_allDefaultReports,
		},
	],
	userPermissions
);
```

### Condition-aware check (single permission + subject instance)

Use this when a rule carries a `$in` / `$eq` condition (e.g. restaurant-scoped permissions). Only accepts a single permission because conditions are subject-specific.

```typescript
const result = checkAccess(
	{ action: PermissionAction.manage, subject: Permission.Subject.Business.restaurant },
	userPermissions,
	{ restaurantId: 'abc' } // evaluated against rule conditions, e.g. { restaurantId: { $in: ['abc', 'def'] } }
);

// Typical use: filter a list to items the user can access
const accessible = restaurants.filter(
	(r) =>
		checkAccess(
			{ action: PermissionAction.manage, subject: Permission.Subject.Business.restaurant },
			userPermissions,
			{ restaurantId: r.id }
		).granted
);
```

### Return value

```typescript
interface AccessCheckResult {
	granted: boolean;
	/** The permission requirement that was decisive. */
	decisivePermission: CaslPermission;
	/** The matched CASL rule, or null if nothing matched. Parse `.reason` as JSON for audit source info. */
	decisiveRule: RawRule | null;
}
```

### Adding a New Permission

> **Tip:** You can ask Copilot to do this for you using the `add-new-permission` skill. Example prompts:
>
> ```
> @workspace /add-new-permission add hrm_payroll under the HRM category with manage action
> ```
>
> ```
> @workspace /add-new-permission add crm_loyaltyCard under CRM category, manage action, behind feature flag crm_loyalty_card
> ```
>
> ```
> @workspace /add-new-permission add report_exportCsv under Report category with manage action
> ```

Adding a portal permission (one admins can toggle in the UI) requires two file changes.

### Step 1 — Add the subject enum value

File: `packages/permission/src/common/types.ts`

```typescript
export enum PermissionSubjectBusinessNamespace {
	// ... existing entries ...

	// HRM module — follow the module_feature naming pattern
	hrm_payroll = 'business::hrm::payroll',
}
```

**Naming rules:**

- Prefix with the module: `crm_`, `inventory_`, `hrm_`, `report_`
- camelCase feature name
- Value pattern: `business::<module>::<feature>`

### Step 2 — Add it to `FullPortalPermissions`

File: `packages/permission/src/common/full-portal-permissions.ts`

```typescript
export const FullPortalPermissions = {
	// ... existing entries ...

	payroll: {
		label: 'Payroll Management', // shown in the UI permission editor and audit logs
		subject: PermissionSubjectBusinessNamespace.hrm_payroll,
		actions: [PermissionAction.manage],
		category: PermissionCategory.hrm, // General | Inventory | HRM | CRM | Report
		// showByFeatureFlag: 'payroll-beta',                // optional: hides checkbox until flag is on
	},
};
```

**Available categories:** `PermissionCategory.general`, `.inventory`, `.hrm`, `.crm`, `.payment`, `.report`, `.reports`, `.customReports`, `.menu`

That's it. The permission will now appear as a checkbox in the HRM portal permission editor under the correct category section.

### Step 3 — Rebuild the package

```bash
cd packages/permission && pnpm build
```

---

<details>
<summary>Advanced: system permission sets (auto-injected resource dependencies)</summary>

Some features silently require access to other resources to work correctly (e.g. "Team Member Management" needs to read the POS role list to populate a dropdown). Rather than requiring admins to grant both, you define a **system permission set** that auto-injects the dependency whenever the parent permission is granted.

### When to use this

- Your feature silently requires read access to another resource → use `permissions[]`
- You split an existing subject into multiple new granular subjects and existing users should silently get them → use `permissionSets[]` (backward-compat chaining)
- If neither applies, skip this — most portal permissions do not need a system set

### Subject namespaces in `permissions[]`

`permissions[]` accepts both resource subjects (`hrm::posRole`) and business/module subjects
(`business::payment::payoutAccount`). Both are injected as leaf-level CASL rules and are never
shown in the portal UI. Use `PermissionSubjectBusinessNamespace` directly when the subject already
exists there — no separate resource enum needed. Create a resource enum only for subjects with no
corresponding portal entry.

### `permissions[]` — inject a read-only dependency

```typescript
// permission-manifest/hrm/hrm-system-sets.ts
import { HrmResource } from './types';

[`set_${PermissionSubjectBusinessNamespace.hrm_teamMember}`]: {
  key: 'sys:team_member_access',   // stable — never change after deploy, stored in audit logs
  name: 'Team Member Access',      // shown in audit log breadcrumbs
  permissions: [
    {
      label: 'Pos Role',           // subject name only, reused as audit log display label
      subject: HrmResource.hrm_posRole,
      actions: [PermissionAction.read],
    },
  ],
},
```

> **Recommended rule: prefer `read`-only actions in `permissions[]` — avoid `manage` where possible.**
> Injecting `manage` causes two problems: (1) **UI bleed** — the editor filters out system-injected
> entries by checking for non-`manage` actions, so a `manage` entry renders as an auto-checked
> checkbox; (2) **over-grant** — `manage` is a CASL wildcard covering all actions, silently granting
> destructive write access the admin never explicitly authorised.
> If `manage` access is needed as a side-effect, prefer `permissionSets[]` chaining instead.
> Some existing system sets (e.g. `hrm-system-sets.ts`) inject `manage` via `permissions[]` for
> legacy reasons — new sets should avoid this pattern.

### `permissionSets[]` — chain to another portal permission (backward-compat)

Use when an existing parent permission should silently unlock new granular sibling subjects. Each chained subject gets a synthetic `manage` rule and its own system set is recursively expanded.

```typescript
[`set_${PermissionSubjectBusinessNamespace.hrm_payroll}`]: {
  key: 'sys:payroll_access',
  name: 'Payroll Access',
  permissionSets: [
    PermissionSubjectBusinessNamespace.hrm_payroll_approval, // users with payroll:manage get this too
  ],
},
```

### File organisation

When a domain has many system sets, extract them:

```
permission-manifest/
  hrm/
    types.ts           ← HrmResource enum
    hrm-system-sets.ts
    index.ts
  inventory/
    types.ts           ← InventoryResource enum
    inventory-system-sets.ts
    index.ts
```

Spread them into `SYSTEM_PERMISSION_SETS` in `system-permission-sets.ts`:

```typescript
export const SYSTEM_PERMISSION_SETS = {
	...INVENTORY_SYSTEM_SETS,
	...HRM_SYSTEM_SETS,
};
```

### Updated checklist (with system set)

| Step                            | File                            | Portal permission | Resource dependency |
| ------------------------------- | ------------------------------- | :---------------: | :-----------------: |
| Add enum value                  | `types.ts`                      |        ✅         |         ✅          |
| Add to `FullPortalPermissions`  | `full-portal-permissions.ts`    |        ✅         |         ❌          |
| Add to `SYSTEM_PERMISSION_SETS` | `permission-manifest/<domain>/` |        ❌         |         ✅          |
| Add `@Action` to controller     | your controller                 |        ✅         |         ✅          |
| Rebuild package                 | —                               |        ✅         |         ✅          |

</details>

<details>
<summary>Vue components & route protection</summary>

```typescript
import '@feedmepos/hrm-permission/style.css';
```

### PermissionWrapper

Renders children only when route permissions are satisfied.

```vue
<template>
	<PermissionWrapper>
		<div>Protected content</div>
	</PermissionWrapper>
</template>

<script setup lang="ts">
import { PermissionWrapper } from '@feedmepos/hrm-permission/components';
</script>
```

### withPermission HOC

Wraps a component so it renders only when the route's `validationManifest` permissions are satisfied. Typically combined with a lazy-loaded component:

```typescript
import { withPermission } from '@feedmepos/hrm-permission/components';
import { withLoading } from '@/components/loading';
import { Permission } from '@feedmepos/hrm-permission';
import type { RouteMeta } from 'vue-router';

// Wrap lazy-loaded views
const hrMain = withPermission(withLoading(() => import('@/views/hr/Main.vue')));
const teamMain = withPermission(withLoading(() => import('@/views/team/Main.vue')));

// Helper to build the required route meta
const canManage = (subject: string): RouteMeta =>
	({
		validationManifest: {
			requiredCaslPermissions: [{ action: Permission.Action.manage, subject }],
		},
	}) as unknown as RouteMeta;

// Routes
const routes = [
	{
		path: '/',
		component: hrMain,
		meta: canManage(Permission.Subject.Business.hrm_employee),
		children: [
			{
				path: 'employee',
				component: withLoading(() => import('@/views/hr/employee/EmployeeList.vue')),
			},
			{ path: 'role', component: withLoading(() => import('@/views/hr/role/RoleList.vue')) },
		],
	},
	{
		path: '/team',
		component: teamMain,
		meta: canManage(Permission.Subject.Business.hrm_teamMember),
	},
];
```

The `validationManifest` on the route meta is read by `withPermission` to check the user's CASL ability before rendering. If the check fails, the component is not mounted.

</details>

<details>
<summary>Backend service & NestJS integration</summary>

### Standalone

```typescript
import { PermissionService } from '@feedmepos/hrm-permission/nestjs';

const service = new PermissionService({
	mongoUrl: process.env.MONGODB_URL,
	dbName: process.env.MONGODB_NAME || 'companyDB',
});

await service.initialize();

const ability = await service.constructAbility({
	userId: 'user123',
	level: 1, // Permission.Level.business
	role: 'admin',
	businessId: 'biz123',
});

await service.close();
```

### NestJS module

```typescript
// app.module.ts
import { PermissionModule } from '@feedmepos/hrm-permission/nestjs';

@Module({
	imports: [
		PermissionModule.forRoot({
			mongoUrl: process.env.MONGODB_URL,
			dbName: process.env.MONGODB_NAME,
		}),
	],
})
export class AppModule {}

// your.service.ts
import { PermissionService } from '@feedmepos/hrm-permission/nestjs';

@Injectable()
export class MyService {
	constructor(private readonly permissionService: PermissionService) {}
}
```

### Environment variables

| Variable       | Required | Description                          |
| -------------- | -------- | ------------------------------------ |
| `MONGODB_URL`  | ✅       | MongoDB connection string            |
| `MONGODB_NAME` | —        | Database name (default: `companyDB`) |

</details>

<details>
<summary>Audit logging</summary>

Audit logging is handled automatically by **hrm-backend** — every `ActionGuard` permission check writes a ClickHouse entry via gRPC. Consumer services do not call any audit-log function directly.

If you need to build a log entry manually (e.g. inside hrm-backend itself), use `buildPermissionLog`:

```typescript
import { buildPermissionLog, type AuditContext } from '@feedmepos/hrm-permission/audit-log';
import { checkAccess } from '@feedmepos/hrm-permission/utils';

const result = checkAccess(
	[{ action: PermissionAction.read, subject: Permission.Subject.Business.hrm_employee }],
	userPermissions
);

const logEntry = buildPermissionLog(result, {
	userId: 'user123',
	requestPath: '/api/employees',
	requestMethod: 'GET',
	businessId: 'biz456',
	country: 'MY',
} satisfies AuditContext);
```

</details>

<details>
<summary>Migration guide</summary>

### Flat → namespaced subjects

`PermissionSubjectBusiness` is `@deprecated`. Use `PermissionSubjectBusinessNamespace` instead. Legacy subjects are automatically remapped at runtime.

| Old (deprecated)            | New                                    |
| --------------------------- | -------------------------------------- |
| `business::promotion`       | `business::crm::promotion`             |
| `business::voucher`         | `business::crm::voucher`               |
| `business::membership`      | `business::crm::membership`            |
| `business::stock`           | `business::inventory::stock`           |
| `business::ingredient`      | `business::inventory::ingredient`      |
| `business::recipe`          | `business::inventory::recipe`          |
| `business::unit`            | `business::inventory::unit`            |
| `business::supplier`        | `business::inventory::supplier`        |
| `business::warehouse`       | `business::inventory::warehouse`       |
| `business::publish`         | `business::inventory::publish`         |
| `business::integration`     | `business::inventory::integration`     |
| `business::orderDraft`      | `business::inventory::orderDraft`      |
| `business::wastageTemplate` | `business::inventory::wastageTemplate` |
| `business::closingTemplate` | `business::inventory::closingTemplate` |
| `business::orderTemplate`   | `business::inventory::orderTemplate`   |
| `business::unitCostHistory` | `business::inventory::unitCostHistory` |
| `business::permission`      | `business::hrm::teamMember`            |
| `business::role`            | `business::hrm::employee::role`        |

### `hasAccess` → `checkAccess`

```typescript
// Before
const canAccess: boolean = hasAccess(requiredPermissions, userPermissions);

// After
const result = checkAccess(requiredPermissions, userPermissions);
const canAccess = result.granted;
```

</details>

<details>
<summary>Package structure & all exports</summary>

| Entry point                            | What's in it                                                                          |
| -------------------------------------- | ------------------------------------------------------------------------------------- |
| `@feedmepos/hrm-permission`            | Enums, types, constants, `checkAccess`                                                |
| `@feedmepos/hrm-permission/utils`      | `checkAccess`, `validate`, `validateRoute`, `mergePermissions`, `mapLegacyPermission` |
| `@feedmepos/hrm-permission/components` | `PermissionWrapper`, `withPermission`                                                 |
| `@feedmepos/hrm-permission/nestjs`     | `PermissionModule`, `PermissionService` + re-exports common                           |
| `@feedmepos/hrm-permission/service`    | `PermissionService` (standalone, no NestJS)                                           |
| `@feedmepos/hrm-permission/audit-log`  | `buildPermissionLog`, `AuditContext` type                                             |
| `@feedmepos/hrm-permission/style.css`  | Component styles                                                                      |

### Peer dependencies

- `@casl/ability`
- `@feedmepos/core`
- `@feedmepos/zod-repo`
- `mongodb`
- `vue` ^3.5.0 _(for components)_
- `vue-router` ^4.2.0 _(for route validation)_

</details>

<details>
<summary>Development</summary>

```bash
pnpm install

pnpm dev        # run demo app with hot reload
pnpm build      # build library
pnpm type-check
```

</details>
