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());
|
|
|
|
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 |
|
Description |
|
Button |
|
Close |
|
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.
Related surfaces
|
The supplier’s Amazon account is suspended — distinct from a Payability suspension. |
|
Persistent suspension banner outside the dashboard. |
|
|