Suspension

A suspended supplier keeps read access and the TRANSFER_FUNDS permission, but loses the ability to draw new advances. The client’s job is to explain why and, where possible, offer a fix.

Reason codes

// src/app/core/pyb/enums/suspension-reason.enum.ts
export enum SuspensionReason {
    AccountAtRisk                   = 'sh.susp:acc_at_risk',
    ApiNeeded                       = 'sh.susp:api_needed',
    Cancellation                    = 'sh.susp:cancellation',
    InvalidCreditCard               = 'sh.susp:inv_credit_card',
    MarketplaceAccessDiverted       = 'sh.susp:access_diverted',
    MarketplacePayeeDiverted        = 'sh.susp:payee_diverted',
    MarketplacePermissionsIncorrect = 'sh.susp:perm_inc',
    SellingPrivilegesRemoved        = 'sh.susp:sell_prv_rmv',
    SuspendedByMarketplace          = 'sh.susp:susp_by_mp',
    FailedPayment                   = 'sh.susp:failed_payments',
}

Only a subset produces a modal:

export const supportedSuspensionReasons = [
    SuspensionReason.AccountAtRisk,
    SuspensionReason.InvalidCreditCard,
    SuspensionReason.MarketplaceAccessDiverted,
    SuspensionReason.MarketplacePayeeDiverted,
    SuspensionReason.MarketplacePermissionsIncorrect,
    SuspensionReason.SellingPrivilegesRemoved,
    SuspensionReason.SuspendedByMarketplace,
    SuspensionReason.FailedPayment,
];

ApiNeeded and Cancellation are deliberately excluded — they are handled by the API-connector banners and the cancellation banners respectively.

Trigger

SuspensionReasonInitializerService.initialize() merges two streams and reacts to either, but only while the supplier is on the dashboard:

this.subscription = merge(this.onToDashboardNavigation(), this.onUserSuspended())
    .subscribe(() => this.isOnDashboardPage() && this.checkUserStatus());

onToDashboardNavigation()

NavigationEnd events whose urlAfterRedirects includes /dashboard.

onUserSuspended()

supplierStatus transitioning to Suspended.

initialize() unsubscribes any prior subscription first, so it is safe to call more than once.

Eligibility for the modal

The reason and its marketplace come from the supplier_suspension dashboard notification’s data (SuspensionNotificationDataDto):

export type SuspensionNotificationDataDto = {
    reasonCode: SuspensionReason;
    reasonCount?: number;
    lastMpStatementEndDate?: string;
    marketplace?: { marketplace_name: string; marketplace_key: string };
};
private isSuspensionReasonSupported = (suspensionData: SuspensionNotificationDataDto): boolean => {
    const isSupported = supportedSuspensionReasons.includes(suspensionData?.reasonCode)
                     && !!suspensionData?.marketplace;                                   (1)
    if (!isSupported) { return false; }

    if (reasonsWithMarketplaceVariance.includes(suspensionData?.reasonCode)) {
        return [MarketplaceEnum.Amazon.valueOf(),
                MarketplaceEnum.Walmart.valueOf(),
                MarketplaceEnum.Payoneer.valueOf()].includes(suspensionData.marketplace.marketplace_key);   (2)
    }
    return true;
};
1 A marketplace is required — a supported reason without one shows nothing.
2 Reasons whose copy varies by marketplace are only shown for Amazon, Walmart and Payoneer; other channels have no written variant.
export const reasonsWithMarketplaceVariance = [
    SuspensionReason.MarketplacePermissionsIncorrect,
    SuspensionReason.MarketplacePayeeDiverted,
    SuspensionReason.MarketplaceAccessDiverted,
];

Copy resolution

export const copyNamespace = 'SUSPENSION_REASON';

export const suspensionMarketplaceMap = {
    [MarketplaceEnum.Amazon]:   'amazon',
    [MarketplaceEnum.Walmart]:  'walmart',
    [MarketplaceEnum.Payoneer]: 'walmart',      (1)
};

export const produceTranslationPath = (reason: SuspensionReason, marketplaceKey?: string): string => {
    const [, reasonCode] = reason.split(':');   (2)

    if (reasonsWithMarketplaceVariance.includes(reason) && Object.keys(suspensionMarketplaceMap).includes(marketplaceKey)) {
        return `${copyNamespace}.${reasonCode}.${suspensionMarketplaceMap[marketplaceKey]}`;
    }
    return `${copyNamespace}.${reasonCode}`;
};
1 Payoneer intentionally reuses the Walmart copy — the remediation steps are the same.
2 The sh.susp: prefix is stripped, so sh.susp:acc_at_risk resolves to SUSPENSION_REASON.acc_at_risk, and the Amazon variant of sh.susp:perm_inc resolves to SUSPENSION_REASON.perm_inc.amazon.

Failed payments

Failed-payment copy escalates with the attempt count:

if (suspensionData.reasonCode === SuspensionReason.FailedPayment) {
    customizedReasonCode = (suspensionData.reasonCode +
        (!suspensionData.reasonCount || suspensionData.reasonCount <= 1 ? '.first' : '.next')) as unknown as SuspensionReason;
}

giving SUSPENSION_REASON.failed_payments.first or .next.

The modal

Built by MessageModalService with:

Title

SUSPENSION_REASON.TITLE

Description

produceTranslationPath(reason, marketplaceKey), interpolating marketplaceName and the full data object.

Button

COMMON.NEXT for Amazon + MarketplaceAccessDiverted; COMMON.CLOSE otherwise. Non-Amazon marketplaces always get COMMON.CLOSE.

Close

hideClose: false.

lastMpStatementEndDate is reformatted from yyyy-MM-dd to MM/dd/yyyy before interpolation, inside a try/catch so an unparseable date does not block the modal.

A this.modal field ensures only one suspension modal exists at a time; it is cleared in afterClosed().

Remediation

One reason has an actionable follow-up:

const shouldUseAmazonHandler =
    suspensionData?.reasonCode === SuspensionReason.MarketplaceAccessDiverted &&
    suspensionData.marketplace.marketplace_key === MarketplaceEnum.Amazon;

this.modal.afterClosed().pipe(take(1)).subscribe(result => {
    if (result && shouldUseAmazonHandler) {
        this.store.dispatch(openAmazonInviteWrapper());   // re-invite Payability on Amazon
    }
    this.modal = null;
});

Amazon access diverted → the modal’s COMMON.NEXT button opens the Amazon invite-Payability wizard directly.

modals/amazon-suspended/

The supplier’s Amazon account is suspended — distinct from a Payability suspension.

BannerService

Persistent suspension banner outside the dashboard.

selectIsSupplierCancelled

Suspended + cancelationStatus === Canceled means cancelled, not suspended — see Cancellation & Reactivation.

Permissions while suspended

Kept

TRANSFER_FUNDS — Active or Suspended grants it.

Lost

ADVANCE_LINE.WITHDRAW_ADVANCE_LINE — explicitly requires supplierStatus !== Suspended.