properties: * * label The name an operator reads on the admin screens, registered * as an English literal. It is shown through display_label(): * a built-in name left as registered in the reader's language * (step 6, translations), a name an add-on gave as it gives it * (careback_status_label). The `status` column keeps the key. * text_key Key of the customer-facing name in Texts (portal, emails). * types Request types the status may be set on; empty means every type. * terminal Nothing may be reached from it (Status_Policy). * settled The money question is answered; no refund clock runs (Deadlines). * closing The request is over without a settlement (Deadlines). * grounds Setting it asks the operator for the grounds (Requests screen). * money It states money went back to the customer. * consuming A request in it holds the units it names. * track_stage Step of the request timeline on the tracking page; '' keeps * the step the request had (a modifier, not a step). * position Sort order on every list; lower first. * * A stored request can carry a status no registration names any more — one an * add-on registered, read after the add-on is gone. Such a status is never * offered and never written, and is shown as its raw value; but what it says * about the request must not change with it, or a closed file reopens and a * held unit is offered again. STORED_ONLY keeps those meanings. * * The free part's own ten statuses stay registered whatever a callback * returns, with the properties its rules read (CORE_PROPERTIES); a callback * may rename one or move it on the list. The list is built once and kept * until the filter's callbacks change (Hook_Cache). */ class Statuses { /** Filter that receives the registered statuses, key => properties. */ const FILTER = 'careback_statuses'; /** * Filter that has the last word on whether a status may be set on a request * of a given type: ( bool $allowed, string $status, string $type, * string $resolution, string $request_status ). */ const ALLOWED_FILTER = 'careback_status_allowed'; /** * Filter: the name an operator reads for a status ( string $name, string * $status, string $label ) — $name as display_label() made it, $label as * registered. An add-on names its own statuses in the reader's language here. */ const LABEL_FILTER = 'careback_status_label'; /** Every property with the value a registration that leaves it out gets. */ const PROPERTIES = array( 'label' => '', 'text_key' => '', 'types' => array(), 'terminal' => false, 'settled' => false, 'closing' => false, 'grounds' => false, 'money' => false, 'consuming' => false, 'track_stage' => 'claim_received', 'position' => 1000, ); /** * Properties of the free part's own statuses that a registration cannot * change: the request types it may be set on and the meanings the free * part's rules read — what is terminal (Status_Policy), what settles or * closes a request, which asks for the grounds, which states money went * back, which holds the units. A callback that returns one of the ten with * other values, or without it, gets them back as defaults() has them. */ const CORE_PROPERTIES = array( 'types', 'terminal', 'settled', 'closing', 'grounds', 'money', 'consuming' ); /** * Meanings kept for statuses stored rows may carry without a registration. * * Data safety, not a list of statuses on offer: none of these is offered or * accepted unless a registration names it, and a registration that does wins * over the line here. A dispatched replacement keeps its units and its * settled refund clock; a partial refund stays settled, terminal and holding; * goods handed back after a guarantee claim stay a closed file. */ const STORED_ONLY = array( 'replacement_sent' => array( 'settled' => true, 'consuming' => true, 'track_stage' => 'claim_settled', ), 'returned_to_customer' => array( 'terminal' => true, 'closing' => true, 'track_stage' => 'claim_settled', ), 'partially_refunded' => array( 'terminal' => true, 'settled' => true, 'money' => true, 'consuming' => true, 'track_stage' => 'claim_settled', ), ); /** * The statuses this plugin registers itself: the pipeline of a withdrawal * and of a return, and the three ways a request ends. * * @return array */ public static function defaults() { return array( 'new' => array( 'label' => 'Received', 'consuming' => true, 'track_stage' => 'claim_received', 'position' => 10, ), 'confirmed' => array( 'label' => 'Confirmed', 'consuming' => true, 'track_stage' => 'claim_accepted', 'position' => 20, ), 'label_sent' => array( 'label' => 'Return instructions sent', 'consuming' => true, 'track_stage' => 'claim_accepted', 'position' => 30, ), 'received' => array( 'label' => 'Parcel received', 'consuming' => true, 'track_stage' => 'claim_arrived', 'position' => 40, ), 'inspected' => array( 'label' => 'Inspected', 'consuming' => true, 'track_stage' => 'claim_checked', 'position' => 50, ), 'refunded' => array( 'label' => 'Refunded', 'terminal' => true, 'settled' => true, 'money' => true, 'consuming' => true, 'track_stage' => 'claim_settled', 'position' => 80, ), // Waiting on the customer does not move the timeline; it is a modifier. 'awaiting_customer' => array( 'label' => 'Waiting for customer', 'consuming' => true, 'track_stage' => '', 'position' => 100, ), 'rejected' => array( 'label' => 'Not accepted', 'terminal' => true, 'closing' => true, 'grounds' => true, 'track_stage' => 'claim_closed', 'position' => 110, ), // A validation flag over a set already counted: it holds nothing, or // the same units would be counted twice. 'duplicate' => array( 'label' => 'Already in progress', 'terminal' => true, 'closing' => true, 'grounds' => true, 'track_stage' => 'claim_closed', 'position' => 120, ), 'cancelled' => array( 'label' => 'Cancelled', 'terminal' => true, 'closing' => true, 'grounds' => true, 'track_stage' => 'claim_closed', 'position' => 130, ), ); } /** * The built-in names in the reader's language, key => name. The registry * keeps the English (defaults()); these are what a screen shows for them. * * @return array */ private static function own_names() { return array( 'new' => _x( 'Received', 'request status', 'careback' ), 'confirmed' => _x( 'Confirmed', 'request status', 'careback' ), 'label_sent' => _x( 'Return instructions sent', 'request status', 'careback' ), 'received' => _x( 'Parcel received', 'request status', 'careback' ), 'inspected' => _x( 'Inspected', 'request status', 'careback' ), 'refunded' => _x( 'Refunded', 'request status', 'careback' ), 'awaiting_customer' => _x( 'Waiting for customer', 'request status', 'careback' ), 'rejected' => _x( 'Not accepted', 'request status', 'careback' ), 'duplicate' => _x( 'Already in progress', 'request status', 'careback' ), 'cancelled' => _x( 'Cancelled', 'request status', 'careback' ), ); } /** * The name an operator reads for a status (step 6, translations): a built-in * name still as registered is shown in the reader's language; a name a * callback of `careback_statuses` changed is shown as written; a status no * registration names, as its raw value. Then `careback_status_label`, where * an add-on names its own. Nothing stored changes: a request keeps the key. * * @param string $status Status value. * @return string */ public static function display_label( $status ) { $status = (string) $status; $all = self::all(); $label = isset( $all[ $status ] ) ? (string) $all[ $status ]['label'] : $status; $own = self::defaults(); $name = $label; if ( isset( $own[ $status ] ) && $own[ $status ]['label'] === $label ) { $names = self::own_names(); $name = isset( $names[ $status ] ) ? $names[ $status ] : $label; } return (string) apply_filters( self::LABEL_FILTER, $name, $status, $label ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- careback_status_label, the final prefix. } /** * Every registered status with the name an operator reads, key => name, in * list order (display_label()). * * @return array */ public static function display_labels() { $out = array(); foreach ( self::keys() as $key ) { $out[ $key ] = self::display_label( $key ); } return $out; } /** * Every registered status with every property set, in list order. * * @return array */ public static function all() { return Hook_Cache::get( self::FILTER, array( __CLASS__, 'build' ) ); } /** * Builds the list: the free part's statuses through `careback_statuses`, * the free part's own put back with their core properties, every property * completed, in list order. Use all(), which keeps the result. * * @return array */ public static function build() { // Registry::build(), shared with the route registry: the free part's own // put back with CORE_PROPERTIES, and stable, so two statuses at the same // position keep the order they were registered in. return Registry::build( self::FILTER, self::defaults(), static function ( $key, array $definition ) { return self::normalise( $key, $definition ); }, self::CORE_PROPERTIES ); } /** * Keys of the registered statuses, in list order: what may be offered, * filtered on and written. * * @return string[] */ public static function keys() { return array_keys( self::all() ); } /** * True when a registration names the status. * * @param string $status Status value. * @return bool */ public static function is_registered( $status ) { return array_key_exists( (string) $status, self::all() ); } /** * Properties of one status: its registration, else the meaning kept for a * stored value (STORED_ONLY), else null. * * @param string $status Status value. * @return array|null */ public static function get( $status ) { $status = (string) $status; $all = self::all(); if ( isset( $all[ $status ] ) ) { return $all[ $status ]; } if ( isset( self::STORED_ONLY[ $status ] ) ) { return self::normalise( $status, self::STORED_ONLY[ $status ] ); } return null; } /** * True when the status carries the property, registered or kept for a * stored value. * * @param string $status Status value. * @param string $property One of the boolean properties. * @return bool */ public static function is( $status, $property ) { $definition = self::get( $status ); return null !== $definition && ! empty( $definition[ $property ] ); } /** * Statuses that carry a boolean property: the registered ones in list * order, then any kept for stored values that no registration names. * * @param string $property One of the boolean properties. * @return string[] */ public static function with( $property ) { $all = self::all(); $out = array(); foreach ( $all as $key => $definition ) { if ( ! empty( $definition[ $property ] ) ) { $out[] = $key; } } foreach ( self::STORED_ONLY as $key => $definition ) { if ( ! isset( $all[ $key ] ) && ! empty( $definition[ $property ] ) ) { $out[] = $key; } } return $out; } /** * Statuses nothing may be reached from. * * @return string[] */ public static function terminal() { return self::with( 'terminal' ); } /** * Statuses that settle the money side, so no refund clock runs. * * @return string[] */ public static function settled() { return self::with( 'settled' ); } /** * Statuses that close a request without settling it. * * @return string[] */ public static function closing() { return self::with( 'closing' ); } /** * Statuses that ask the operator for the grounds of closing. * * @return string[] */ public static function asking_grounds() { return self::with( 'grounds' ); } /** * Statuses that state money went back to the customer. * * @return string[] */ public static function money() { return self::with( 'money' ); } /** * Statuses in which a request holds the units it names. * * @return string[] */ public static function consuming() { return self::with( 'consuming' ); } /** * Registered statuses a request is still being worked in: not settled, not * closed, not terminal. * * @return string[] */ public static function open() { $out = array(); foreach ( self::all() as $key => $definition ) { if ( empty( $definition['terminal'] ) && empty( $definition['settled'] ) && empty( $definition['closing'] ) ) { $out[] = $key; } } return $out; } /** * The operator's name of a status; the raw value when no registration * names it. * * @param string $status Status value. * @return string */ public static function label( $status ) { $all = self::all(); return isset( $all[ (string) $status ] ) ? $all[ (string) $status ]['label'] : (string) $status; } /** * Operator names of the registered statuses, key => name, in list order. * * @return array */ public static function labels() { return wp_list_pluck( self::all(), 'label' ); } /** * Texts keys of the customer-facing names of the registered statuses, * key => Texts key, in list order. * * @return array */ public static function text_keys() { return wp_list_pluck( self::all(), 'text_key' ); } /** * True when a status may be set on a request of this type. * * A status no registration names is never allowed — it is shown, not * offered — and the filter is not asked about it. A registered one is * allowed when it names no types or names this one, and * `careback_status_allowed` has the last word. * * @param string $status Status being set. * @param string $type Type of the request. * @param string $resolution Resolution recorded on the request. * @param string $request_status Status the request holds now. * @return bool */ public static function allowed_for_type( $status, $type, $resolution = '', $request_status = '' ) { $status = (string) $status; $type = (string) $type; $all = self::all(); if ( ! isset( $all[ $status ] ) ) { return false; } $allowed = empty( $all[ $status ]['types'] ) || in_array( $type, $all[ $status ]['types'], true ); return (bool) apply_filters( self::ALLOWED_FILTER, $allowed, $status, $type, (string) $resolution, (string) $request_status ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- careback_status_allowed, the final prefix. } /** * One definition with every property present and typed. * * @param string $key Status value. * @param array $definition Properties as registered. * @return array */ private static function normalise( $key, array $definition ) { $out = array_merge( self::PROPERTIES, array_intersect_key( $definition, self::PROPERTIES ) ); $out['key'] = $key; $out['label'] = '' !== (string) $out['label'] ? (string) $out['label'] : $key; $out['text_key'] = '' !== (string) $out['text_key'] ? (string) $out['text_key'] : 'status_' . $key; $out['types'] = array_values( array_map( 'strval', (array) $out['types'] ) ); $out['track_stage'] = (string) $out['track_stage']; $out['position'] = (int) $out['position']; foreach ( array( 'terminal', 'settled', 'closing', 'grounds', 'money', 'consuming' ) as $flag ) { $out[ $flag ] = (bool) $out[ $flag ]; } return $out; } }