reports = $reports; $this->settings = $settings; } /** * Registers the routes. * * @since 0.1.0 * * @return void */ public function register_routes() { $permission = Permissions::require_capability( Capabilities::VIEW_REPORTS ); $range = array( 'from' => array( 'type' => 'string', 'default' => '', ), 'to' => array( 'type' => 'string', 'default' => '', ), ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/dashboard', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'dashboard' ), 'permission_callback' => $permission, ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/summary', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'summary' ), 'permission_callback' => $permission, 'args' => $range, ) ); } /** * Numbers for the dashboard: counts, this month and last 12 months. * * @since 0.1.0 * * @return WP_REST_Response */ public function dashboard(): WP_REST_Response { $today = Date::today_gmt(); $month = substr( $today, 0, 8 ) . '01'; $from = Date::add_months( $month, -11 ); $counts = $this->reports->dashboard_counts( $today, $month, Date::add_days( $today, 30 ) ); $totals = $this->reports->totals( $month, $today ); $counts['overdue_amount_formatted'] = $this->settings->format_money( $counts['overdue_amount'] ); $counts['outstanding_formatted'] = $this->settings->format_money( $counts['outstanding'] ); return rest_ensure_response( array( 'counts' => $counts, 'month' => array_merge( $totals, array( 'net' => $totals['received'] - $totals['refunded'], 'received_formatted' => $this->settings->format_money( $totals['received'] - $totals['refunded'] ), 'invoiced_formatted' => $this->settings->format_money( $totals['invoiced'] ), ) ), 'revenue' => $this->with_money( $this->fill_months( $this->reports->revenue_by_month( $from, $today ), $from, 12 ) ), 'aging' => $this->with_money( $this->reports->aging( $today ), 'amount' ), ) ); } /** * The reports for a date range. * * @since 0.1.0 * * @param WP_REST_Request $request Request: `from`, `to` (`YYYY-MM-DD`). * @return WP_REST_Response|WP_Error */ public function summary( $request ) { $range = $this->range( (string) $request['from'], (string) $request['to'] ); if ( is_wp_error( $range ) ) { return $range; } list( $from, $to ) = $range; $totals = $this->reports->totals( $from, $to ); return rest_ensure_response( array( 'from' => $from, 'to' => $to, 'totals' => array_merge( $totals, array( 'net' => $totals['received'] - $totals['refunded'], 'received_formatted' => $this->settings->format_money( $totals['received'] ), 'refunded_formatted' => $this->settings->format_money( $totals['refunded'] ), 'invoiced_formatted' => $this->settings->format_money( $totals['invoiced'] ), 'tax_formatted' => $this->settings->format_money( $totals['tax'] ), 'net_formatted' => $this->settings->format_money( $totals['received'] - $totals['refunded'] ), ) ), 'by_month' => $this->with_money( $this->reports->revenue_by_month( $from, $to ) ), 'by_method' => $this->with_money( $this->reports->revenue_by_method( $from, $to ) ), 'tax' => array_map( function ( array $row ) { $row['net_formatted'] = $this->settings->format_money( $row['net'] ); $row['tax_formatted'] = $this->settings->format_money( $row['tax'] ); return $row; }, $this->reports->tax_by_rate( $from, $to ) ), 'aging' => $this->with_money( $this->reports->aging( Date::today_gmt() ), 'amount' ), 'top_customers' => $this->with_money( $this->reports->top_customers( $from, $to, 10 ) ), ) ); } /** * Validates a date range; defaults to the last 12 months. * * @param string $from From. * @param string $to To. * @return array{0: string, 1: string}|WP_Error */ private function range( string $from, string $to ) { $today = Date::today_gmt(); $to = '' === $to ? $today : $to; $from = '' === $from ? Date::add_months( substr( $today, 0, 8 ) . '01', -11 ) : $from; if ( ! Date::is_valid_date( $from ) || ! Date::is_valid_date( $to ) ) { return $this->error( 'hostkhata_invalid_range', __( 'Choose valid dates.', 'hostkhata' ), 400 ); } if ( $from > $to ) { return $this->error( 'hostkhata_invalid_range', __( 'The start date must not be after the end date.', 'hostkhata' ), 400 ); } if ( Date::days_between( $from, $to ) > self::MAX_DAYS ) { return $this->error( 'hostkhata_invalid_range', __( 'Choose a period of five years or less.', 'hostkhata' ), 400 ); } return array( $from, $to ); } /** * Adds a formatted amount next to each `total` (or the named key). * * @param array> $rows Rows. * @param string $key Amount key. * @return array> */ private function with_money( array $rows, string $key = 'total' ): array { foreach ( $rows as &$row ) { $row[ $key . '_formatted' ] = $this->settings->format_money( (int) $row[ $key ] ); } return $rows; } /** * Gives every month in the range a row, so a chart has no gaps. * * @param array> $rows Rows from the query. * @param string $start First day of the first month. * @param int $months Number of months. * @return array> */ private function fill_months( array $rows, string $start, int $months ): array { $by_month = array(); foreach ( $rows as $row ) { $by_month[ $row['month'] ] = $row; } $result = array(); for ( $i = 0; $i < $months; $i++ ) { $key = substr( Date::add_months( $start, $i ), 0, 7 ); $result[] = $by_month[ $key ] ?? array( 'month' => $key, 'total' => 0, 'payments' => 0, ); } return $result; } }