Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/CRISTIANCAMACH34/Zippi/llms.txt

Use this file to discover all available pages before exploring further.

Zippi’s permission model goes beyond coarse module-level gates. For sensitive operations — editing a product price, closing a cashier shift, refunding an order — there are dedicated fine-grained action permissions that must be checked explicitly. The backend is the authority; the frontend only mirrors permissions for rendering decisions and must never be trusted as the sole gatekeeper.

Permission Structure

Every permission follows the pattern <module>.<action>. All permissions are registered in the PERMISSIONS dictionary in backend/app/modules/auth/domain/rbac.py. The source file is the single source of truth — never infer permissions from a role’s label or from the frontend. Most modules expose at least two permissions:
PermissionMeaning
<module>.readView or list resources in this module
<module>.manageCreate, edit, or delete resources in this module
Examples: orders.read, orders.manage, catalog.read, catalog.manage, cashier.read, cashier.reconcile, liquidations.read, liquidations.manage.

Umbrella vs. Fine-Grained Permissions

Some module-level manage permissions are umbrella permissions: they imply a set of finer-grained action permissions. Others are standalone and do not carry such implications.

Umbrella permission: catalog.manage

catalog.manage expands to all catalog.* action permissions. When a role has catalog.manage, calling expand_permissions() adds every fine-grained catalog action:
CATALOG_ACTION_PERMISSIONS = frozenset(
    permission
    for permission in PERMISSIONS
    if permission.startswith("catalog.") and permission != "catalog.manage"
)

IMPLIED_PERMISSIONS: dict[str, frozenset[str]] = {
    "catalog.manage": CATALOG_ACTION_PERMISSIONS,
}
The full set of catalog action permissions defined in rbac.py:
PermissionDescription
catalog.createCreate a product in the catalog
catalog.edit_nameEdit a product’s name
catalog.edit_priceEdit a product’s sale price
catalog.edit_costEdit a product’s cost
catalog.edit_taxesEdit a product’s taxes
catalog.edit_imageEdit a product’s image
catalog.edit_availabilityMark a product as sold out or available
catalog.duplicateDuplicate a product
catalog.archiveArchive a product
catalog.deleteLogically delete a product
catalog.restoreRestore an archived product
catalog.exportExport the catalog
catalog.importImport catalog data
catalog.change_categoryChange a product’s category
catalog.move_branchMove a product between branches
catalog.change_supplierChange a product’s supplier
catalog.assign_promotionsAssign promotions to a product

Fine-grained order permissions (no umbrella expansion)

Order transition permissions are standaloneorders.manage does not imply them. Each transition requires its own explicit permission check:
PermissionDescription
orders.acceptAccept a pending order
orders.rejectReject a pending order
orders.prepareMark order as in preparation
orders.packMark order as packed
orders.assign_courierAssign a courier to the order
orders.pickupMark order as picked up
orders.transitMark order as en route
orders.deliverMark order as delivered
orders.closeClose a delivered order
orders.cancelCancel an order
orders.refundRefund an order
orders.rescheduleReschedule an order
orders.mark_failedMark an order as failed
orders.mark_absentMark a customer as absent
orders.returnMark an order as returned
orders.collect_paymentRegister payment collection for an order

Fine-grained cashier permissions

PermissionDescription
cashier.openOpen a cashier shift
cashier.closeClose a cashier shift (arqueo)
cashier.reconcileReconcile cash, collections, and Wompi

Operative Role Permissions

The four operative roles (kitchen, cashier, waiter, driver) have tightly constrained permission sets defined in OPERATIVE_ROLE_PERMISSIONS in rbac.py. These are intentionally narrow — each role can do exactly what its job requires and nothing more.
The kitchen role can advance order preparation and mark products as unavailable. It has no access to financial data, admin functions, or liquidations.
OPERATIVE_ROLE_PERMISSIONS["kitchen_staff"] = [
    "orders.read",
    "orders.prepare",
    "orders.pack",
    "kitchen.read",
    "kitchen.manage",
    "catalog.read",
    "catalog.edit_availability",
]
Can: Receive command tickets, advance preparation (Esperando preparación → Preparando → Empacado), mark products as sold out.Cannot: Access cashier or payments, view liquidations, manage admin users, or see analytical dashboards.
The waiter role manages table-side order intake, communicates with the kitchen, and interfaces with the cashier for payment. It cannot see financial totals or manage the catalog.
OPERATIVE_ROLE_PERMISSIONS["waiter"] = [
    "orders.read",
    "orders.accept",
    "orders.reject",
    "waiter.read",
    "waiter.manage",
    "kitchen.read",
    "cashier.read",
]
Can: Accept or reject incoming orders (Pendiente aceptación → Aceptado/Cancelado), manage tables and commands, view kitchen state, read cashier information.Cannot: Close the cashier shift, view payment reconciliation, manage catalog prices, or access liquidations.
The cashier role owns the full till lifecycle: opening the shift, registering payments, performing the arqueo, and closing the shift. It reads orders for payment context but does not advance delivery states.
OPERATIVE_ROLE_PERMISSIONS["cashier"] = [
    "orders.read",
    "orders.close",
    "orders.collect_payment",
    "cashier.read",
    "cashier.reconcile",
    "cashier.open",
    "cashier.close",
    "payments.read",
    "reports.read",
]
Can: Open and close shifts, collect order payments, reconcile cash vs. Wompi, read payment records, read reports.Cannot: Advance kitchen preparation states, manage the catalog, view courier fleet, or manage liquidations.
The driver role can only interact with their own assigned deliveries. Scope type is self. Evidence upload and incident reporting are included.
OPERATIVE_ROLE_PERMISSIONS["delivery_driver"] = [
    "orders.read",
    "orders.pickup",
    "orders.transit",
    "orders.deliver",
    "orders.mark_failed",
    "orders.mark_absent",
    "couriers.read",
    "evidence.read",
    "evidence.manage",
    "incidents.read",
]
Can: Pick up orders, mark them as in-transit, delivered, failed, or absent; upload evidence photos; report incidents; view their own courier profile.Cannot: See orders belonging to other drivers or other businesses, access cashier, manage catalog, or view liquidations.

Implied Permission Expansion

expand_permissions() takes a list of permissions and adds all implied (child) permissions before the final set is returned. This is called at JWT construction time and again at every permission check.
def expand_permissions(permissions: list[str] | set[str]) -> list[str]:
    expanded = set(permissions)
    for umbrella, implied in IMPLIED_PERMISSIONS.items():
        if umbrella in expanded:
            expanded.update(implied)
    return sorted(expanded)
Currently only catalog.manage triggers expansion. Any new umbrella→children relationship must be added to IMPLIED_PERMISSIONS.

Checking a Permission at Runtime

Use has_expanded_permission() to check whether a user has a specific permission after expansion:
def has_expanded_permission(user: dict | None, permission: str) -> bool:
    if not user:
        return False
    return permission in set(expand_permissions(list(user.get("permissions") or [])))
In Flask route handlers, use the require_permission decorator from backend/app/modules/auth/presentation/guards.py:
from app.modules.auth.presentation.guards import require_permission

@bp.route("/catalog/products/<int:pid>/price", methods=["PATCH"])
@require_permission("catalog.edit_price")
def update_price(pid: int):
    user = get_current_user()
    scope = business_scope_from_user(user)
    # ... business logic
For endpoints that accept multiple alternative permissions:
from app.modules.auth.presentation.guards import require_any_permission

@bp.route("/orders/<int:oid>/accept", methods=["POST"])
@require_any_permission("orders.accept", "orders.manage")
def accept_order(oid: int):
    ...

The FE↔BE Coherence Rule

The frontend uses permissions exclusively for rendering decisions — showing or hiding buttons, links, and UI sections. It does not enforce security. The backend re-validates every request regardless of what the frontend claims.
RuleConsequence of violation
Permission exists in FE but not in BEVulnerability — user can see UI but API still denies
Permission exists in BE but not in FEHidden feature — no security issue, but poor UX
rbac.py changed without updating permissions.tsFE/BE desync — UI may show/hide incorrectly
rbac.py changed without updating tests/by_role/Silent regression — permissions appear correct but are untested
The synchronization chain when adding or changing a permission:
1. backend/app/modules/auth/domain/rbac.py   ← source of truth
2. backend/app/modules/business/security/business_permissions.py   ← if portal-gated
3. Endpoint guard (require_permission / require_any_permission)
4. frontend/src/modules/business/permissions.ts   ← FE mirror
5. backend/tests/by_role/<role>/test_access.py    ← can / cannot assertions
6. pytest tests/by_role -q   ← must be green before merging

Fail-Closed Default

When a role or permission is not found, the system denies by default. There is no fallthrough that grants access on an unknown role:
def can(role: str, permission: str) -> bool:
    definition = ROLE_DEFINITIONS.get(role)
    if definition is None:
        return False          # unknown role → deny
    return permission in definition["permissions"]
This applies at every layer: unknown roles, missing permissions, missing scope — all result in denial (403 or 404), never in silent grant.

Notable Permission Caveats

AssumptionReality
kitchen_staff has orders.manage to advance preparationWrong — kitchen uses the fine-grained action permissions orders.prepare and orders.pack directly; the umbrella orders.manage is not in OPERATIVE_ROLE_PERMISSIONS["kitchen_staff"]
customer has orders.manage to track their orderWrong — customer has only orders.read; they cannot transition states
support_agent can access cashier/paymentsWrong — support has no cashier, payments, or liquidations permissions
operations_admin handles financial reconciliationWrong — operations has no cashier, payments, or liquidations permissions
Always check rbac.py directly. Never infer permissions from a role’s label.

Build docs developers (and LLMs) love