/**
* Dedicated @wordpress/data store for the bulk-labels modal's
* fetch-once resources.
*
* The orders shipping-context is a GET-by-ids collection, so it's a
* core-data *entity* (see register-entity.ts). These two aren't:
* - GET /wcshipping/v1/packages is a singleton object, and
* - POST /wcshipping/v1/label/auto-assign-packages is body-driven,
* neither of which maps onto a core-data entity. A small store with
* resolvers is the idiomatic @wordpress/data tool for them — it gives
* the same automatic caching / dedupe / resolution-state the entity
* pattern does, without each hook hand-rolling apiFetch + useState.
*/
import { createReduxStore } from '@wordpress/data';
import { apiFetch, controls as wpControls } from '@wordpress/data-controls';
import { addQueryArgs } from '@wordpress/url';
import { BULK_LABELS_STORE_NAME } from 'data/constants';
import {
getAutoAssignPackagesPath,
getBatchLabelPurchasePath,
getPackagesPath,
} from 'data/routes';
// Mirror the single-label rate/purchase flow: without this the
// /wcshipping/v1/packages response filters out the FedEx & UPSDAP
// predefined schema, so saved predefined boxes lose their dimensions
// (dropped from the dropdown) and auto-assigned FedEx/UPSDAP boxes
// can't be rated.
const FEATURES_SUPPORTED_BY_CLIENT = [ 'upsdap', 'fedex' ];
import {
buildAssignablePackages,
type RawPackagesResponse,
} from './packages-transform';
import type {
AssignablePackage,
AutoAssignedPackagesMap,
BatchPurchaseResponse,
BatchPurchaseShipment,
} from './types';
interface BulkLabelsState {
assignablePackages?: AssignablePackage[];
autoAssigned: Record< string, AutoAssignedPackagesMap >;
}
const DEFAULT_STATE: BulkLabelsState = {
assignablePackages: undefined,
autoAssigned: {},
};
const SET_ASSIGNABLE_PACKAGES = 'SET_ASSIGNABLE_PACKAGES';
const SET_AUTO_ASSIGNED_PACKAGES = 'SET_AUTO_ASSIGNED_PACKAGES';
/** Stable cache key for an auto-assign batch, order-independent. */
const autoAssignKey = ( orderIds: number[] ): string =>
[ ...orderIds ].sort( ( a, b ) => a - b ).join( ',' );
const actions = {
setAssignablePackages( packages: AssignablePackage[] ) {
return { type: SET_ASSIGNABLE_PACKAGES, packages } as const;
},
setAutoAssignedPackages( key: string, results: AutoAssignedPackagesMap ) {
return { type: SET_AUTO_ASSIGNED_PACKAGES, key, results } as const;
},
*purchaseBatchLabels(
origin: Record< string, unknown >,
shipments: BatchPurchaseShipment[],
signal?: AbortSignal
): Generator<
ReturnType< typeof apiFetch >,
BatchPurchaseResponse,
BatchPurchaseResponse
> {
const response: BatchPurchaseResponse = yield apiFetch( {
path: getBatchLabelPurchasePath(),
method: 'POST',
data: { origin, shipments },
signal,
} );
return response ?? {};
},
};
type Action =
| ReturnType< typeof actions.setAssignablePackages >
| ReturnType< typeof actions.setAutoAssignedPackages >;
const reducer = (
state: BulkLabelsState = DEFAULT_STATE,
action: Action
): BulkLabelsState => {
switch ( action.type ) {
case SET_ASSIGNABLE_PACKAGES:
return { ...state, assignablePackages: action.packages };
case SET_AUTO_ASSIGNED_PACKAGES:
return {
...state,
autoAssigned: {
...state.autoAssigned,
[ action.key ]: action.results,
},
};
default:
return state;
}
};
const selectors = {
getAssignablePackages(
state: BulkLabelsState
): AssignablePackage[] | undefined {
return state.assignablePackages;
},
getAutoAssignedPackages(
state: BulkLabelsState,
orderIds: number[]
): AutoAssignedPackagesMap | undefined {
return state.autoAssigned[ autoAssignKey( orderIds ) ];
},
};
const resolvers = {
*getAssignablePackages() {
const response: RawPackagesResponse = yield apiFetch( {
path: addQueryArgs( getPackagesPath(), {
features_supported_by_client: FEATURES_SUPPORTED_BY_CLIENT,
} ),
} );
return actions.setAssignablePackages(
buildAssignablePackages( response )
);
},
*getAutoAssignedPackages( orderIds: number[] ) {
if ( ! orderIds || orderIds.length === 0 ) {
return actions.setAutoAssignedPackages(
autoAssignKey( orderIds ?? [] ),
{}
);
}
const response: AutoAssignedPackagesMap = yield apiFetch( {
path: getAutoAssignPackagesPath(),
method: 'POST',
data: { order_ids: orderIds },
} );
return actions.setAutoAssignedPackages(
autoAssignKey( orderIds ),
response ?? {}
);
},
};
/**
* Shape of `select( BULK_LABELS_STORE_NAME )` — the data selectors plus
* the resolution-state meta-selectors @wordpress/data adds for any
* selector that has a resolver. Lets the hooks stay typed without
* pulling the (lazily-registered) store descriptor.
*/
export interface BulkLabelsSelect {
getAssignablePackages: () => AssignablePackage[] | undefined;
getAutoAssignedPackages: (
orderIds: number[]
) => AutoAssignedPackagesMap | undefined;
hasFinishedResolution: ( selector: string, args: unknown[] ) => boolean;
getResolutionError: ( selector: string, args: unknown[] ) => unknown;
}
export interface BulkLabelsDispatch {
purchaseBatchLabels: (
origin: Record< string, unknown >,
shipments: BatchPurchaseShipment[],
signal?: AbortSignal
) => Promise< BatchPurchaseResponse >;
}
/**
* Normalize whatever `getResolutionError()` hands back into an `Error`.
* The data layer may return the raw REST error object (`{ code,
* message }`) rather than an `Error`, so an `instanceof Error` check
* alone silently drops real failures.
*/
export const toError = ( value: unknown ): Error | null => {
if ( ! value ) {
return null;
}
if ( value instanceof Error ) {
return value;
}
if ( typeof value === 'string' ) {
return new Error( value );
}
if ( typeof value === 'object' ) {
const maybe = value as { message?: unknown; code?: unknown };
if ( typeof maybe.message === 'string' && maybe.message ) {
return new Error( maybe.message );
}
if ( typeof maybe.code === 'string' && maybe.code ) {
return new Error( maybe.code );
}
}
return new Error( 'Request failed.' );
};
export const createBulkLabelsStore = () =>
createReduxStore( BULK_LABELS_STORE_NAME, {
reducer,
actions,
selectors,
resolvers,
controls: wpControls,
} );