Utility Reference

The small shared building blocks live in two places, and the split is historical rather than principled:

core/_base/

Left over from the Metronic admin template the project was scaffolded from. Layout services, generic CRUD primitives, pipes and directives.

core/pyb/utils/, core/pyb/constants/, core/pyb/animations/, core/pyb/interfaces/

Payability-authored helpers.

Nothing here holds business rules. When a helper starts encoding product behaviour it belongs in a service under core/pyb/services/ instead — see Service Reference.

Subscriber — the subscription base class

core/pyb/utils/subscriber.class.ts

@Component({ selector: 'subscriber', template: '' })
export abstract class Subscriber implements OnDestroy {
    protected subscriptions: Subscription[] = [];

    ngOnDestroy() {
        this.subscriptions.forEach(subscription => subscription.unsubscribe());
    }

    addSubscription(sub: Subscription): Subscriber { /* … */ }
    addSubscriptions(...sub: Subscription[]): Subscriber { /* … */ }
}

This is the most-extended class in the codebase — around 109 classes inherit from it. Subclasses push their subscriptions in and get teardown for free.

Subscriber is decorated with @Component, not @Directive or @Injectable, purely so Angular’s compiler accepts the ngOnDestroy. Two ESLint rules are disabled inline to allow the bare subscriber selector and the missing class suffix. Do not copy this pattern for new base classes; use @Directive() without a selector, which is the supported Angular idiom.

A subclass that declares its own ngOnDestroy must call super.ngOnDestroy() or every subscription leaks.

Components that manage their own teardown instead use a Subject plus takeUntil, usually named unsubscribe. Both patterns are common; follow whichever the surrounding file uses.

String, object and date helpers — core/pyb/utils/utils.ts

dictToSelectOptions(dict)

Turns a { key: label } map into the SelectOption[] shape pyb-dynamic-field expects. Fed by the dictionaries in core/pyb/dictionaries/ and by report filter maps.

snakeToCamel, snakeToPascal, camelToSnake, camelToPascal, pascalToCamel

Case conversion between API (snake_case) and client (camelCase) naming. camelToSnake is the one used most, for report filter parameters.

formatDateProperties(obj, dateFormat)

Walks an object and formats every Date-valued property with PybDate. Drops falsy values entirely. Report data sources call this on filter objects before sending them.

formatDate(date)

Single date to MM/dd/yyyy. Returns null for anything unparseable.

decodeAuthToken(token)

JWT payload as AuthTokenDecoded.

sortByProperty(property, sortOrder, mapProperty?)

Comparator factory for Array.sort(). mapProperty projects the value before comparison, e.g. to sort by a date’s epoch.

returnOnlyNumbersFromString(value)

Strips non-digits — phone numbers, account numbers.

addPrefix(prefix, value)

Template-literal concatenation. Exists so app-config.ts can compose dial-able phone numbers declaratively.

loadExternalScript(url, id)

Appends a <script> to <head> and emits once it loads. Used by user-details.component.ts to inject the Zendesk widget (ze-snippet) on demand rather than on every page load.

sortByProperty returns -1 * sortOrderValue when either value is falsy, so records with an empty sort field are not merely ordered last — they compare inconsistently, which can make Array.sort() produce a different order for the same input depending on the initial arrangement. Prefer server-side sorting for anything a user will notice.

Two more single-purpose helpers sit alongside:

jwt-decode.ts — jwtDecode(jwtToken)

The same JWT payload decode as decodeAuthToken, untyped. Both exist; jwtDecode is the one used in LoginComponent.

remove-empty.ts — removeEmpty(obj)

Recursively drops undefined properties. Note it keeps null and empty strings, and recurses into every object-typed value including arrays and Date instances, which it will turn into plain objects.

ads-checker.ts — checkIsAdBlockerEnabled()

Returns true when window.pybCRA is undefined. The flag is set by assets/js/pyb-ads.js, a decoy script whose filename ad blockers match, so its absence means a blocker is active. Read once by instant-transfer-setup.modal.ts to show adblockWarning — Plaid’s iframe does not load with a blocker running.

errors.ts — NotImplementedError

Thrown by unimplemented base-class methods such as BaseDataSource.nextPage().

PybDate

core/pyb/utils/pyb-date.ts

A thin wrapper over date-fns with a Moment-like fluent API, introduced when Moment was removed. It supports only three units of time:

export type PybDateType   = string | Date | number;
export type PybUnitOfTime = 'days' | 'week' | 'year';

new PybDate(date?)

Defaults to now. Any input is stringified then run through parseISO.

PybDate.now()

Epoch milliseconds.

PybDate.nowUtc()

Today at UTC midnight — used for date-picker minimums so the boundary does not shift by timezone.

format(dateFormat)

Delegates to date-fns format.

diff(date, unitOfTime)

Difference in days, weeks or years.

add(amount, unit), subtract(amount, unit)

Mutate the instance and return this.

isBefore(date)

Instant comparison.

get(unitOfTime)

'days' returns the day of the week, 'week' an ISO-ish week number, 'year' the full year.

toDate(), toString()

Unwrap.

Three sharp edges:

  • add and subtract mutate in place. const d = new PybDate(x); d.add(1, 'days'); changes d. Construct a fresh instance when you need the original.

  • The constructor accepts a dateFormat second argument and ignores it.

  • Passing a numeric epoch produces parseISO('1699999999999'), which is not a valid parse. Wrap epochs in new Date() first.

  • get('days') returns the weekday index, not the day of the month, despite the unit name.

Generic CRUD primitives — core/_base/crud/

Exported from core/_base/crud/index.ts.

BaseModel

Template scaffolding: _isEditMode, _userId, _createdDate, _updatedDate. Inherited by User, Role and Permission and otherwise unused.

BaseDataSource

The template’s data source. Superseded — see the warning below.

QueryParamsModel<F>

filter, sortOrder, sortField, pageNumber, pageSize (default 20). The report and AG Grid code all speak this shape. Re-exported by @payability/pyb-ng2-api too, and the API’s copy is the one report data sources import.

QueryResultsModel<T>

items, totalCount, errorMessage.

HttpExtenstionsModel

In-memory baseFilter, sortArray and searchInArray over an array — a client-side stand-in for a paginated endpoint. The spelling of the class name is a typo that is now load-bearing.

HttpUtilsService

Wraps HttpExtenstionsModel and builds HttpParams. Provided in AppModule and injected nowhere.

TypesUtilsService

Number padding and MM/dd/yyyy date string conversion. Also provided in AppModule and injected nowhere.

There are two classes named BaseDataSource:

  • core/_base/crud/models/_base.datasource.ts — the template original, used only by core/auth/_data-sources/ and the legacy core/reports/ components.

  • core/pyb/models/base/data-source/base.datasource.ts — the generic BaseDataSource<T> that every real report extends. Adds loadError$, hasItems$, setEntityData() and fetch-subscription cancellation.

New code wants the second one. Check the import path when reading report code. See Reports for the data source lifecycle.

HttpUtilsService and TypesUtilsService are provided in AppModule but have no injection sites left. They are safe to delete once someone confirms no external consumer imports them.

Pipes — core/_base/layout/pipes/

Each pipe ships with its own NgModule that both declares and provides it, so a feature module imports JoinModule rather than declaring JoinPipe.

Pipe Module Behaviour

safe

SafeModule

bypassSecurityTrustHtml / Style / Script / Url / ResourceUrl depending on the type argument, defaulting to HTML. Every use is a deliberate hole in Angular’s sanitizer — only pass server-controlled or translated strings.

join

JoinModule

Space-joins an array; passes non-arrays through unchanged.

getObject

GetObjectModule

objectPath.get(value, path) for safe nested access.

firstLetter

FirstLetterModule

Initials: splits on spaces and takes the first character of each word, uppercased. Used for avatar placeholders.

addPercent

AddPercentModule

Appends %. Returns '' for falsy input, so a genuine 0 renders as empty.

maskedBankAccount

MaskedBankAccountModule

Renders •••• 1234 from the last four characters. Emits HTML entities, so the result must be bound with innerHTML, not interpolated. Throws on null.

replaceSentence

ReplaceSentenceModule

value.replace(sentence, newValue) — first occurrence only, since the needle is a string rather than a global regex.

core/pyb/pipes/filter-by.pipe.ts adds filterBy. Currency formatting comes from PybCurrencyAccountingModule in @payability/pyb-ngx-components.

Directives — core/_base/layout/directives/

ktContentAnimate

Fades and slides content in on every NavigationEnd. It skips the animation when the URL contains the string content-animation-blocked. A route that should not re-animate navigates with contentAnimationBlockerParam (core/_base/layout/models/content-animate-blocker.model.ts) in its query params, which puts the key in the URL:

export const contentAnimationBlockerKey   = 'content-animation-blocked';
export const contentAnimationBlockerParam = { [contentAnimationBlockerKey]: true };

The two-factor setup page uses this so stepping through its sub-states does not replay the page transition.

pybDisabled

Not styling — it toggles the form control’s disabled state, which keeps the control out of form.value and out of validation:

@Directive({ selector: '([formControlName], [formControl])[pybDisabled]' })
export class PybDisabledDirective implements OnInit {
    @Input() set pybDisabled(disabled: boolean) {
        this.disabled = disabled;
        this.setDisabled();
    }
    // resolves the control from the host ControlContainer, then calls
    // control.disable() / control.enable()
}

The selector requires a formControlName or formControl on the same element. Without a resolvable control the directive logs a console.warn and does nothing. Non-boolean values are ignored, so [pybDisabled]="someString" fails silently.

Use this instead of the disabled attribute on reactive-form inputs — Angular warns about mixing disabled with formControlName, and this directive is the project’s answer to that.

ktSticky

An extended fork of angular-sticky-things. Throttles scroll and resize to animationFrameScheduler, then sets position: sticky plus inline top, width, z-index and background-color on the host, toggling the is-sticky and boundary-reached host classes.

[scrollContainer]

Selector or element to observe instead of window.

[spacerElement]

A sibling whose height absorbs the host when it goes sticky. Omitting it logs a dev-mode warning and the page will jump.

[boundaryElement]

Element the sticky region may not scroll past.

[marginTop], [marginBottom]

Offsets, in pixels.

[enable]

Turns stickiness off, emitting once more so the element un-sticks.

recalculate()

Public; call after layout changes that move the element.

makeSticky() hard-codes background-color: #fff inline, which defeats dark mode on sticky elements. It also sets width in pixels, so a sticky element does not reflow until the next resize event.

Animations — core/pyb/animations/

One trigger, parameterised by height:

export const collapseExpandAnimation = (height: number, duration = '500ms') =>
    trigger('inOutAnimation', [
        transition(':enter', [style({ height: 0 }), animate(`${duration} ease-out`,  style({ height }))]),
        transition(':leave', [style({ height }),   animate(`${duration} ease-in`,  style({ height: 0 }))]),
    ]);

Because the trigger name is fixed at inOutAnimation, a component can register it only once. The caller supplies the expanded height, so the animation cannot adapt to content that measures itself.

Constants — core/pyb/constants/

contact.const.ts

contacts.supportEmail and contacts.supportPhone, both read from APP_CONFIG. Templates should use this rather than hard-coding support details.

country-codes.ts

US_CODE, re-exported from constants/index.ts. Drives the US-only branches in address and bank forms.

help-articles.const.ts

Zendesk article URLs, currently just reconciliationReportExplanation.

ltc-flow.constants.ts

LtcContractActionType — New, Renew, Change. See Long-Term Contract.

suspension-reason.constants.ts

Which suspension reasons are supported, which are Amazon-only, and produceTranslationPath(). See Suspension.

two-factor-banners.const.ts

Banner copy keys for the 2FA nudge. See Two-Factor Authentication.

re-sign-contract-banner.const.ts

Banner copy keys for the re-sign prompt.

APP_CONFIG

core/_config/app-config/app-config.ts — the one place support contact details are declared. Phone numbers are stored once in display form and derived into dial-able form at module load:

[PybDepartments.CUSTOMER_EXPERIENCE]: {
    VIEW:        PhoneNumbers.CUSTOMER_EXPERIENCE,
    ONLY_NUMBER: addPrefix(PhoneNumbersPrefixes.US, returnOnlyNumbersFromString(PhoneNumbers.CUSTOMER_EXPERIENCE)),
},

EMAILS holds CUSTOMER_EXPERIENCE, SALES, SUPPORT_API, PARTNERS, ACCOUNTS and SUSPENSION. The suspension address is an individual’s mailbox rather than a group alias, which is worth revisiting.

Shared interfaces

core/pyb/interfaces/ holds the cross-feature shapes that are not API DTOs — Banner and BannerOptions, Action, Tooltip, AdCard, AuthTokenDecoded, DocusignHandlerParams, ApprovedProducts, InstantAdvanceTerms, UnifiedSupplierCard and others. Field-level detail lives with the feature that owns each one; see Model Reference and Domain Model.

Two type helpers are worth knowing:

core/pyb/utils/generic-form-group.interface.ts

GenericFormGroup<V> narrows FormGroup so value is typed V and controls is Record<keyof V, FormControl>. Use it on form-heavy components to get compile-time checking of control names. See Forms and Fields.

core/_base/interfaces/

BreadcrumbTitle, plus the CountryCode, PcardStatus and PcardTypeIndicator enums. Enum values are catalogued in Enum Reference.