import { find, flatMap, get, isEmpty, isEqual, omit, sortBy } from 'lodash';
// @ts-ignore can't find type definitions for @woocommerce/settings
import { getSetting } from '@woocommerce/settings';
import { composeAddress, composeName } from '../order';
import {
Destination,
LocationResponse,
OriginAddress,
WCShippingConfig,
} from 'types';
import {
ACCEPTED_USPS_ORIGIN_COUNTRIES,
US_MILITARY_STATES,
US_TERRITORY_STATES,
} from './constants';
import { getConfig } from '../config';
import { camelCaseKeys } from '../common';
const getContinents = ( config: WCShippingConfig ) =>
get( config, [ 'continents' ] );
export const getCountryName =
( config: WCShippingConfig = getConfig() ) =>
( countryCode: string ) => {
const country = find( flatMap( getContinents( config ), 'countries' ), {
code: countryCode,
} );
if ( ! country ) {
return countryCode;
}
return country.name;
};
export const getAllCountries = ( config: WCShippingConfig ) => {
const allCountries = flatMap( getContinents( config ), 'countries' );
return sortBy( allCountries, 'name' );
};
export const getAllCountryNames = (
config: WCShippingConfig
): {
label: string;
value: string;
}[] => {
const countries = getAllCountries( config );
return countries.reduce( ( acc, { code, name } ) => {
return [
...acc,
{
label: name,
value: code,
},
];
}, [] );
};
const getOriginCountryNames = (
config: WCShippingConfig
): ReturnType< typeof getAllCountryNames > =>
getAllCountryNames( config ).filter( ( { value } ) =>
ACCEPTED_USPS_ORIGIN_COUNTRIES.includes( value )
);
export const getCountryNames = (
group: 'destination' | 'origin' | 'all',
countryCode: string | undefined | null,
config = getConfig()
) => {
let countryNames =
group === 'origin'
? getOriginCountryNames( config )
: getAllCountryNames( config );
// If the selected country is not supported but the user managed to select it, add it to the list
if (
countryCode &&
! countryNames.find( ( c ) => countryCode === c.value )
) {
countryNames = [
...countryNames,
{
label: countryCode,
value: getCountryName( config )( countryCode ),
},
];
}
return countryNames;
};
export const getStates = (
countryCode: string,
config: WCShippingConfig = getConfig()
) => {
const country = find( flatMap( getContinents( config ), 'countries' ), {
code: countryCode,
} );
if ( ! country ) {
return [];
}
return sortBy( country.states, 'name' );
};
export const getStateNames = (
countryCode: string,
config: WCShippingConfig = getConfig()
) => {
const states = getStates( countryCode, config );
let names: Record< string, string > = states.reduce(
( acc, { code, name } ) => {
const key = `${ code }`.startsWith( `${ countryCode }-` )
? `${ code }`.replace( `${ countryCode }-`, '' )
: code;
return {
...acc,
[ key ]: name,
};
},
{} as Record< string, string >
);
if ( countryCode === 'US' ) {
// Filter out military addresses
names = omit( names, US_MILITARY_STATES );
}
return Object.entries( names ).reduce(
( acc, [ value, label ] ) => [
...acc,
{
label,
value,
},
],
[] as {
label: string;
value: string;
}[]
);
};
export const hasStates = (
countryCode: string,
config: WCShippingConfig = getConfig()
) => {
return ! isEmpty( getStates( countryCode, config ) );
};
export const getStoreOrigin = () => {
return {
country: getSetting( 'baseLocation' )?.country ?? 'US',
state: getSetting( 'baseLocation' )?.state ?? 'CA',
};
};
/**
* US territories can be addressed either as their own country code (after
* address normalization) or as country "US" with the territory in the state
* field (when the merchant declines normalization). Collapse both encodings to
* a single effective country code so customs handling is consistent regardless
* of which form the address takes.
*/
const getEffectiveCustomsCountry = (
address: Pick< LocationResponse, 'country' | 'state' >
): string | undefined => {
if (
address?.country === 'US' &&
US_TERRITORY_STATES.includes( address?.state )
) {
return address.state;
}
return address?.country;
};
export const isCustomsRequired = (
origin: Pick< LocationResponse, 'country' | 'state' >,
destination: Pick< LocationResponse, 'country' | 'state' >
) => {
if (
! isEqual(
getEffectiveCustomsCountry( origin ),
getEffectiveCustomsCountry( destination )
)
) {
return true;
}
if (
origin?.country === 'US' &&
US_MILITARY_STATES.includes( origin?.state )
) {
return true;
}
if (
destination?.country === 'US' &&
US_MILITARY_STATES.includes( destination?.state )
) {
return true;
}
return false;
};
/**
* USPS enforces the customs `itemDescription` 30-char limit only on its
* domestic-mail label path (US + US territories + military APO/FPO/DPO, which
* are country `US`). True international labels go through a different path and
* are not subject to it. A shipment uses the domestic-mail path when both
* endpoints are USPS domestic-mail territories — mirrors the backend
* `USPSTerritories::DOMESTIC_MAIL_TERRITORIES`.
*/
export const isUSPSDomesticMailShipment = (
originCountry?: string,
destinationCountry?: string
): boolean => {
if ( ! originCountry || ! destinationCountry ) {
return false;
}
return (
ACCEPTED_USPS_ORIGIN_COUNTRIES.includes( originCountry ) &&
ACCEPTED_USPS_ORIGIN_COUNTRIES.includes( destinationCountry )
);
};
export const getOriginAddresses = (
config: WCShippingConfig = getConfig()
) => {
return ( config.origin_addresses ?? [] )
.map( ( originalAddress ) => ( {
...originalAddress,
address: composeAddress( originalAddress ),
name: composeName( originalAddress ),
} ) )
.map( camelCaseKeys );
};
export const getFirstSelectableOriginAddress = (
config: WCShippingConfig = getConfig()
) => {
const originAddresses = getOriginAddresses( config );
const defaultAddress = originAddresses.find(
( address ) => address.defaultAddress
);
if ( defaultAddress ) {
return defaultAddress;
}
// Prioritize store address with ID 'store_details' as default return address
const storeAddress = originAddresses.find(
( address ) => address.id === 'store_details'
);
return (
storeAddress ??
( originAddresses.length > 0 ? originAddresses[ 0 ] : undefined )
);
};
export const getDefaultOutboundAddress = (
config: WCShippingConfig = getConfig()
) => {
const originAddresses = getOriginAddresses( config );
const defaultAddress = originAddresses.find(
( address ) => address.defaultAddress
);
if ( defaultAddress ) {
return defaultAddress;
}
// Fallback to first selectable address
return getFirstSelectableOriginAddress( config );
};
export const getDefaultReturnAddress = (
config: WCShippingConfig = getConfig()
) => {
const originAddresses = getOriginAddresses( config );
const defaultReturnAddress = originAddresses.find(
( address ) => address.defaultReturnAddress
);
if ( defaultReturnAddress ) {
return defaultReturnAddress;
}
// Fallback to first selectable address
return getFirstSelectableOriginAddress( config );
};
export const isOriginAddress = (
address: Destination | OriginAddress
): address is OriginAddress => 'id' in address;
export const isCountryInEU = (
countryCode: string,
config: WCShippingConfig = getConfig()
) => config.eu_countries.includes( countryCode.toUpperCase() );