Permissions System

Permissions are calculated on the client, from the User object, every time the user is loaded or their status changes. They are not returned by the API and not stored server-side. If a supplier can or cannot see something, the answer is in src/app/core/pyb/services/permissions.service.ts.

Pipeline

userLoaded / userUpdated / userStatusUpdated
  → userPermissionsRequested
  → AuthEffects.loadedUserPermissions$
        PermissionsService.getUserPermissions(user)
            getDefaultPermissions()              always granted
          + calculatePermissionsFromUserState()  derived from User.status
          + getDevelopPermissions()              non-production only
          → de-duplicated via Set
  → userPermissionsLoaded({ permissions })
  → permissionsReducer stores hierarchical Permission nodes
  → GlobalSubscriptionsService.loadRolesWithPermissions()
        flushPermissions() then addPermission() for every node,
        prefixing children with the parent name
  → NgxPermissionsService now answers *ngxPermissionsOnly and NgxPermissionsGuard
getUserPermissions(user: User): PermissionsEnum[] {
    const permissions = [...this.getDefaultPermissions(), ...this.calculatePermissionsFromUserState(user)];
    const developPermissions = this.getDevelopPermissions();
    if (!this.environmentService.production) {
        console.group('Please be aware those permissions are available only on dev environments');
        console.log(developPermissions.map(p => PermissionsEnum[p]));
        console.groupEnd();
        permissions.push(...developPermissions);
    }
    return Array.from(new Set(permissions));
}

Recalculation triggers

Action Effect

userLoaded

Fired after getUserByToken() at login or session restore.

userUpdated

AuthEffects.userUpdated$ → userPermissionsRequested.

userStatusUpdated

AuthEffects.userStatusUpdated$ → userPermissionsRequested.

userStatusUpdateRequested

Re-fetches getSupplierStatus(id) → userStatusUpdated → recalculation.

Any code that changes a supplier’s eligibility — a completed withdrawal, a signed contract, an activation — dispatches userStatusUpdateRequested() so permissions catch up.

Default permissions

Granted to every authenticated supplier, unconditionally.

Permission Grants

MY_MARKETPLACES

The marketplace management page.

DASHBOARD

The dashboard.

BETA

Beta feature flag.

MANAGE_TRANSFER_INFORMATION

Transfer-information settings screen.

LONG_TERM_CONTRACT

Long Term Contract features.

CHANGE_PASSWORD

Password change in security settings.

TWO_FACTOR_SETUP

2FA configuration.

SHOPIFY_API_CONNECTION

Shopify connector monitoring and reconnect prompts.

AMAZON_API_CONNECTION

Amazon connector monitoring and reconnect prompts.

DARK_MODE

Dark-mode toggle.

BETA is granted by getDefaultPermissions() but is not present in the permissions.const.ts tree, so it never reaches NgxPermissionsService. It is usable through the store selector but not through *ngxPermissionsOnly.

Development-only permissions

getDevelopPermissions() returns [NEW_EGG_PROVIDER], added only when environment.production is false, and logged to the console as a reminder.

Anything gated on NEW_EGG_PROVIDER works in dev and staging but is invisible in production. Do not use it to gate a feature that is expected to ship.

Advance Line permissions

calculateAdvanceLinePermissionsFromUser(user).

The whole block short-circuits when there is no Advance Line, or when advanceLine.type === AdvanceLineModeEnum.Legacy. Legacy suppliers get no Advance Line permissions — their product surface is Instant Advance.

Three intermediate predicates:

const isAdvanceLineExpired  = new Date() > new Date(advanceLine.expirationDate);
const isAdvanceLineActive   = advanceLine.status   === AdvanceLineStatusEnum.Active;
const isAdvanceLineRejected = advanceLine.uwStatus === AdvanceLineUWStatusEnum.Rejected;
Permission Condition

VIEW_ADVANCE_LINE_WIDGET

isAdvanceLineActive or isUserPendingAndExpedited(user) or isAdvanceLineExpired

ADVANCE_LINE_EXPIRED

isAdvanceLineExpired and not active and not rejected

ADVANCE_LINE_REJECTED

isAdvanceLineRejected or (isAdvanceLineExpired and isAdvanceLineActive)

ADVANCE_LINE_PENDING_ACTIVATION

isUserPending(supplierStatus) and advanceLine.status === Pending and user.schema === OnboardingSchemaEnum.Expedited

WITHDRAW_ADVANCE_LINE

advanceLine.outstandingAmount > 0 and supplierStatus !== Suspended

VIEW_ADVANCE_LINE_REPORT

advanceLine.currentAmount > 0

ADVANCE_LINE_PENDING_ACTIVATION checks OnboardingSchemaEnum.Expedited (exp_onb) specifically. AmazonExpedited (amzn_exp_onb) does not satisfy it, because isUserPendingAndExpedited() also compares against Expedited only.

Instant Advance permissions

calculateInstantAdvancePermissionsFromUser(user).

const hasValidActiveAdvance =
    status.activeInstantAdvance &&
    (!status.advanceLine || status.advanceLine?.type === AdvanceLineModeEnum.Legacy);

const hasValidPendingAdvance =
    status.advanceLine?.type   === AdvanceLineModeEnum.Legacy &&
    status.advanceLine?.status === AdvanceLineStatusEnum.Pending;
Permission Condition

INSTANT_ADVANCE_REPORT + INSTANT_ADVANCE_REPORT_MVP

status.hasInstantAdvHistory (both granted together)

INSTANT_ADVANCE_REPORT_WIDGET

hasValidActiveAdvance — an active Instant Advance and either no Advance Line at all or a legacy one

INSTANT_ADVANCE_PENDING

hasValidPendingAdvance and isUserPendingAndExpedited(user)

This is the mutual exclusion between the two advance products: a supplier on a modern Advance Line never sees the Instant Advance widget.

Remaining state-based permissions

calculatePermissionsFromUserState(user).

Permission Condition

TRANSFER_INFORMATION

!isUserPending(supplierStatus) or status.pmtCfgProvided

VENDOR_PAY

status.hasForwardPaymentEnabled

REFERRAL

!isUserDuringOnboarding(user)

TRANSFER_FUNDS

supplierStatus === Active or supplierStatus === Suspended

VIEW_NOTIFICATIONS

!isUserPendingAndExpedited(user)

REPORTS + DAILY_SUMMARY

status.hasPaymentHistory and !isUserDuringOnboarding(user) (granted together)

SELLER_CARD

isSellerCardAvailable(user) — see below

LCT_HISTORY

status.hasLTCHistory

INSTANT_TRANSFER_METHOD

user.ppmAvailable

A Suspended supplier keeps TRANSFER_FUNDS. Suspension restricts new advances and card usage, not access to money already owed.

Seller Card eligibility

private isSellerCardAvailable(user: User): boolean {
    const hasPaymentHistory = user?.status?.hasPaymentHistory;
    const pcardAvailable    = user?.pcardAvailable;
    const isFromUS          = this.isFromUS(user);
    return hasPaymentHistory && (pcardAvailable || isFromUS) && !this.isUserDuringOnboarding(user);
}

private isFromUS(user: User): boolean {
    const country            = user?._businessInformation?.address?.country;
    const residentialCountry = user?._personalInformation?.residentialAddress?.country;
    return [country, residentialCountry].every(code => code === CountryCode.US);
}

isFromUS() requires both the business address and the residential address to be US. A US-incorporated business with an overseas owner fails the test and must be enabled explicitly through pcardAvailable.

Shared predicates

private isUserPending = (s: SupplierStatusType): boolean =>
    s === SupplierStatusType.PendingActivation || s === SupplierStatusType.Pending;

private isUserPendingAndExpedited = (user: User): boolean =>
    this.isUserPending(user.status.supplierStatus) && user.schema === OnboardingSchemaEnum.Expedited;

private isUserDuringOnboarding = (user: User): boolean =>
    user.dashboardStatus?.status === SupplierDashboardStatusType.Onboarding;

The permission enum

src/app/core/pyb/enums/permissions.enum.ts — a numeric enum, so values are positional indices. Reordering members changes every persisted numeric value; always append.

export enum PermissionsEnum {
    ADVANCE_LINE,
    ADVANCE_LINE_EXPIRED,
    ADVANCE_LINE_PENDING_ACTIVATION,
    AMAZON_API_CONNECTION,
    BETA,
    CHANGE_PASSWORD,
    DAILY_SUMMARY,
    DASHBOARD,
    GOD_MODE,
    INSTANT_ADVANCE_REPORT,
    INSTANT_ADVANCE_REPORT_WIDGET,
    INSTANT_ADVANCE_REPORT_MVP,
    VIEW_INSTANT_ADVANCE_REPORT_MVP_ELEMENT,
    INSTANT_TRANSFER_METHOD,
    LCT_HISTORY,
    LONG_TERM_CONTRACT,
    MANAGE_TRANSFER_INFORMATION,
    PROD_MODE,
    REFERRAL,
    REPORTS,
    SELLER_CARD,
    SHOPIFY_API_CONNECTION,
    TRANSFER_FUNDS,
    TRANSFER_INFORMATION,
    TWO_FACTOR_SETUP,
    VIEW_ADVANCE_LINE_REPORT,
    VIEW_ADVANCE_LINE_WIDGET,
    VIEW_NOTIFICATIONS,
    WITHDRAW_ADVANCE_LINE,
    INSTANT_ADVANCE_PENDING,
    DARK_MODE,
    NEW_EGG_PROVIDER,
    MY_MARKETPLACES,
    ADVANCE_LINE_REJECTED,
    VENDOR_PAY,
}

GOD_MODE and PROD_MODE are declared and present in the permission tree but are never granted by PermissionsService.

The permission tree

src/app/core/pyb/services/permissions.const.ts turns the flat enum into a hierarchy. Children are addressed with a dotted, parent-prefixed name.

const permissions = [
    { id: PermissionsEnum.DASHBOARD },
    { id: PermissionsEnum.SELLER_CARD },
    {
        id: PermissionsEnum.REPORTS,
        children: [
            { id: PermissionsEnum.INSTANT_ADVANCE_REPORT },
            { id: PermissionsEnum.INSTANT_ADVANCE_REPORT_MVP },
            { id: PermissionsEnum.VIEW_INSTANT_ADVANCE_REPORT_MVP_ELEMENT },
            { id: PermissionsEnum.DAILY_SUMMARY },
        ],
    },
    { id: PermissionsEnum.TRANSFER_FUNDS },
    { id: PermissionsEnum.GOD_MODE },
    {
        id: PermissionsEnum.ADVANCE_LINE,
        children: [
            { id: PermissionsEnum.ADVANCE_LINE_EXPIRED },
            { id: PermissionsEnum.ADVANCE_LINE_REJECTED },
            { id: PermissionsEnum.VIEW_ADVANCE_LINE_WIDGET },
            { id: PermissionsEnum.VIEW_ADVANCE_LINE_REPORT },
            { id: PermissionsEnum.WITHDRAW_ADVANCE_LINE },
            { id: PermissionsEnum.ADVANCE_LINE_PENDING_ACTIVATION },
        ],
    },
    // …flat entries for the remaining permissions
];

The default export is an IIFE that walks the tree and builds Permission nodes with name, level, parentId and children.

Resulting names

Enum member ngx-permissions name

DASHBOARD

DASHBOARD

REPORTS

REPORTS

DAILY_SUMMARY

REPORTS.DAILY_SUMMARY

INSTANT_ADVANCE_REPORT

REPORTS.INSTANT_ADVANCE_REPORT

INSTANT_ADVANCE_REPORT_MVP

REPORTS.INSTANT_ADVANCE_REPORT_MVP

VIEW_INSTANT_ADVANCE_REPORT_MVP_ELEMENT

REPORTS.VIEW_INSTANT_ADVANCE_REPORT_MVP_ELEMENT

ADVANCE_LINE

ADVANCE_LINE

VIEW_ADVANCE_LINE_WIDGET

ADVANCE_LINE.VIEW_ADVANCE_LINE_WIDGET

WITHDRAW_ADVANCE_LINE

ADVANCE_LINE.WITHDRAW_ADVANCE_LINE

ADVANCE_LINE_PENDING_ACTIVATION

ADVANCE_LINE.ADVANCE_LINE_PENDING_ACTIVATION

SELLER_CARD

SELLER_CARD

Guards and template directives must use the full dotted name. This is why DocusignVerificationService calls removePermission('ADVANCE_LINE.ADVANCE_LINE_PENDING_ACTIVATION') and the daily-summary route asks for REPORTS.DAILY_SUMMARY, not the bare names.

ADVANCE_LINE and REPORTS are parents that PermissionsService never grants directly. A child such as ADVANCE_LINE.WITHDRAW_ADVANCE_LINE is registered whether or not its parent was granted, because GlobalSubscriptionsService.loadRolesWithPermissions() walks whatever nodes the reducer produced.

Consuming permissions

In templates

<div *ngxPermissionsOnly="['SELLER_CARD']">
    <pyb-seller-cards-widget></pyb-seller-cards-widget>
</div>

<div *ngxPermissionsExcept="['ADVANCE_LINE.ADVANCE_LINE_EXPIRED']">
    <pyb-advance-line-widget></pyb-advance-line-widget>
</div>

In routes

{
    path: 'daily-summary-report',
    canActivate: [NgxPermissionsGuard],
    data: { permissions: { only: ['REPORTS.DAILY_SUMMARY'] } },
}

In the navigation menu

MenuConfig items carry a permission string; items the user lacks are hidden.

From code

this.store.select(currentUserPermissions);                       // Permission[] from the store
this.ngxPermissionsService.hasPermission('SELLER_CARD');         // Promise<boolean>
this.ngxPermissionsService.removePermission('INSTANT_ADVANCE_PENDING');

Business flows revoke permissions optimistically after a successful action so the UI updates before userStatusUpdateRequested() round-trips.

Adding a permission

  1. Append the member to PermissionsEnum — never insert in the middle.

  2. Add it to the tree in permissions.const.ts, nested if it belongs under a parent.

  3. Grant it from getDefaultPermissions() or calculatePermissionsFromUserState().

  4. Use the full dotted name in guards, templates and MenuConfig.

  5. Extend permissions.service.spec.ts with the new condition.