$row */ public static function fromApi( array $row ): ?self { $code = is_string( $row['serviceCode'] ?? null ) ? $row['serviceCode'] : ''; $details = is_array( $row['priceDetails'] ?? null ) ? $row['priceDetails'] : array(); $due = is_numeric( $details['due'] ?? null ) ? (float) $details['due'] : null; $taxes = self::taxTotal( $details['taxes'] ?? null ); $base = null !== $due && null !== $taxes ? round( $due - $taxes, 2 ) : ( is_numeric( $details['base'] ?? null ) ? (float) $details['base'] : null ); if ( '' === $code || ( null === $base && null === $due ) ) { return null; } $standard = is_array( $row['serviceStandard'] ?? null ) ? $row['serviceStandard'] : array(); $transit = is_numeric( $standard['expectedTransitTime'] ?? null ) ? (int) $standard['expectedTransitTime'] : null; $guaranteed = filter_var( $standard['guaranteedDelivery'] ?? false, FILTER_VALIDATE_BOOLEAN ); $date = is_string( $standard['expectedDeliveryDate'] ?? null ) ? self::datePart( $standard['expectedDeliveryDate'] ) : null; return new self( $code, is_string( $row['serviceName'] ?? null ) ? $row['serviceName'] : $code, $base ?? (float) $due, $due ?? (float) $base, $transit, $date, $guaranteed ); } public function price( string $taxMode ): float { return self::TAX_DUE === $taxMode ? $this->due : $this->base; } /** @return array{c: string, n: string, b: float, d: float, t: ?int, dd: ?string, g: bool} */ public function toArray(): array { return array( 'c' => $this->serviceCode, 'n' => $this->serviceName, 'b' => $this->base, 'd' => $this->due, 't' => $this->transitDays, 'dd' => $this->deliveryDate, 'g' => $this->guaranteed, ); } /** * @param array $data From toArray(), through the rate cache. */ public static function fromArray( array $data ): ?self { if ( ! is_string( $data['c'] ?? null ) || ! is_numeric( $data['b'] ?? null ) || ! is_numeric( $data['d'] ?? null ) ) { return null; } return new self( $data['c'], is_string( $data['n'] ?? null ) ? $data['n'] : $data['c'], (float) $data['b'], (float) $data['d'], is_int( $data['t'] ?? null ) ? $data['t'] : null, is_string( $data['dd'] ?? null ) ? $data['dd'] : null, true === ( $data['g'] ?? false ) ); } /** * GST + PST + HST from `priceDetails.taxes`, each either `{amt, percent}` * or a bare number. Null when there is no taxes block or one entry can't * be read, so the caller falls back to Canada Post's `base` rather than * treating an unread tax as zero and charging tax twice. */ private static function taxTotal( mixed $taxes ): ?float { if ( ! is_array( $taxes ) ) { return null; } $total = 0.0; foreach ( array( 'gst', 'pst', 'hst' ) as $kind ) { $tax = $taxes[ $kind ] ?? null; if ( null === $tax ) { continue; } $amount = is_array( $tax ) ? ( $tax['amt'] ?? null ) : $tax; if ( ! is_numeric( $amount ) ) { return null; } $total += (float) $amount; } return $total; } /** "2026-10-14" from "2026-10-14" or "2026-10-14T00:00:00.000Z"; null otherwise. */ private static function datePart( string $value ): ?string { return 1 === preg_match( '/^(\d{4}-\d{2}-\d{2})/', $value, $match ) ? $match[1] : null; } }