count( 'sa_invoice' ) + $this->count( 'sa_estimate' ) > 0; } public function preview(): array { $invoices = $this->ids( 'sa_invoice' ); $quotes = $this->ids( 'sa_estimate' ); $already = 0; foreach ( $invoices as $id ) { if ( $this->alreadyImported( \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE, (string) $id ) ) { $already++; } } foreach ( $quotes as $id ) { if ( $this->alreadyImported( \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE, (string) $id ) ) { $already++; } } return [ 'clients' => $this->count( 'sa_client' ), 'invoices' => count( $invoices ), 'quotes' => count( $quotes ), 'payments' => $this->count( 'sa_payment' ), 'already' => $already, ]; } public function run(): array { foreach ( $this->ids( 'sa_estimate' ) as $id ) { $d = $this->document( $id, 'estimate' ); $d['status'] = $this->estimateStatus( get_post_status( $id ) ); $d['expiry_date'] = $this->toDate( get_post_meta( $id, '_expiration_date', true ) ); $this->createQuote( $d ); } $invoice_map = []; foreach ( $this->ids( 'sa_invoice' ) as $id ) { $d = $this->document( $id, 'invoice' ); $d['status'] = $this->invoiceStatus( get_post_status( $id ) ); $d['due_date'] = $this->toDate( get_post_meta( $id, '_due_date', true ) ); $new = $this->createInvoice( $d ); if ( $new > 0 ) { $invoice_map[ $id ] = $new; } } foreach ( $this->ids( 'sa_payment', [ 'publish', 'payment-partial', 'refunded', 'void', 'authorized' ] ) as $pid ) { $inv = (int) get_post_meta( $pid, '_payment_invoice', true ); if ( ! isset( $invoice_map[ $inv ] ) ) { $mapped = $this->alreadyImported( \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE, (string) $inv ); if ( $mapped <= 0 ) { continue; } $invoice_map[ $inv ] = $mapped; } $status = get_post_status( $pid ); $this->createPayment( $invoice_map[ $inv ], [ 'source_id' => (string) $pid, 'amount' => $this->toNumber( get_post_meta( $pid, '_amount', true ) ), 'date' => get_post_field( 'post_date', $pid ), 'method' => (string) get_post_meta( $pid, '_payment_method', true ) ?: 'imported', 'transaction_id' => (string) get_post_meta( $pid, '_trans_id', true ), 'status' => in_array( $status, [ 'publish', 'payment-partial' ], true ) ? 'completed' : ( 'refunded' === $status ? 'refunded' : ( 'void' === $status ? 'failed' : 'pending' ) ), 'notes' => (string) get_post_field( 'post_title', $pid ), ] ); } return $this->report(); } /** * Shared fields, normalised. * * @param int $id Sprout post id. * @param string $kind 'invoice' or 'estimate'. * @return array */ private function document( int $id, string $kind ): array { $post = get_post( $id ); $sa_client = (int) get_post_meta( $id, '_client_id', true ); $client_id = $this->client( $sa_client ); $client_post = $sa_client ? get_post( $sa_client ) : null; $items = []; foreach ( (array) get_post_meta( $id, '_doc_line_items', true ) as $row ) { if ( ! is_array( $row ) ) { continue; } $desc = wp_strip_all_tags( (string) ( $row['desc'] ?? '' ) ); $items[] = [ 'name' => strtok( $desc, "\n" ) ?: __( 'Item', 'easy-invoice' ), 'description' => trim( (string) substr( $desc, strlen( strtok( $desc, "\n" ) ?: '' ) ) ), 'quantity' => $this->toNumber( $row['qty'] ?? 1 ), 'price' => $this->toNumber( $row['rate'] ?? 0 ), 'adjust_percentage' => $this->toNumber( $row['tax'] ?? 0 ), 'taxable' => true, ]; } $issue = $this->toDate( get_post_meta( $id, '_' . $kind . '_issue_date', true ) ) ?: substr( $post->post_date, 0, 10 ); return [ 'source_id' => (string) $id, 'title' => (string) $post->post_title, 'number' => (string) get_post_meta( $id, '_' . $kind . '_id', true ) ?: (string) $id, 'issue_date' => $issue, 'created' => $post->post_date, 'client_id' => $client_id, 'customer_name' => $client_post ? $client_post->post_title : '', 'customer_email' => $client_id ? (string) get_userdata( $client_id )->user_email : '', 'customer_address' => $client_post ? $this->address( (array) get_post_meta( $client_post->ID, '_address', true ) ) : '', 'items' => $items, 'tax_rate' => $this->toNumber( get_post_meta( $id, '_doc_tax', true ) ), 'discount_type' => 'percentage', 'discount_value' => $this->toNumber( get_post_meta( $id, '_doc_discount', true ) ), 'currency_code' => strtoupper( (string) get_post_meta( $id, '_doc_currency', true ) ), 'notes' => wp_strip_all_tags( (string) get_post_meta( $id, '_' . $kind . '_notes', true ) ), 'terms' => wp_strip_all_tags( (string) get_post_meta( $id, '_doc_terms', true ) ), ]; } /** * Map a Sprout client post to ours. * * @param int $sa_client Sprout client post id. * @return int */ private function client( int $sa_client ): int { if ( $sa_client <= 0 ) { return 0; } $post = get_post( $sa_client ); if ( ! $post ) { return 0; } $email = ''; $first = ''; $last = ''; $users = get_post_meta( $sa_client, '_associated_users', false ); foreach ( (array) $users as $uid ) { $u = get_userdata( (int) $uid ); if ( $u ) { $email = $u->user_email; $first = $u->first_name; $last = $u->last_name; break; } } if ( '' === $first && '' === $last ) { list( $first, $last ) = $this->splitName( $post->post_title ); } return $this->findOrCreateClient( [ 'source_id' => (string) $sa_client, 'email' => $email, 'first_name' => $first, 'last_name' => $last, 'business' => (string) $post->post_title, 'address' => $this->address( (array) get_post_meta( $sa_client, '_address', true ) ), 'phone' => (string) get_post_meta( $sa_client, '_phone', true ), 'website' => (string) get_post_meta( $sa_client, '_website', true ), ] ); } /** * Sprout's address array as lines. * * @param array $a street, street_2, city, zone, postal_code, country. * @return string */ private function address( array $a ): string { $lines = array_filter( [ trim( (string) ( $a['street'] ?? '' ) ), trim( (string) ( $a['street_2'] ?? '' ) ), trim( implode( ' ', array_filter( [ $a['city'] ?? '', $a['zone'] ?? '', $a['postal_code'] ?? '' ] ) ) ), trim( (string) ( $a['country'] ?? '' ) ), ] ); return implode( "\n", $lines ); } private function invoiceStatus( string $status ): string { $map = [ 'temp' => 'draft', 'future' => 'draft', 'publish' => 'available', 'partial' => 'available', 'complete' => 'paid', 'write-off' => 'cancelled', 'archived' => 'cancelled' ]; return $map[ $status ] ?? 'available'; } private function estimateStatus( string $status ): string { $map = [ 'temp' => 'draft', 'request' => 'available', 'publish' => 'sent', 'future' => 'draft', 'approved' => 'accepted', 'declined' => 'declined', 'archived' => 'declined' ]; return $map[ $status ] ?? 'available'; } /** * Ids of a Sprout post type. * * @param string $type Post type. * @param string[] $statuses Post statuses to include. * @return int[] */ private function ids( string $type, array $statuses = [ 'temp', 'publish', 'partial', 'complete', 'write-off', 'archived', 'request', 'approved', 'declined', 'future' ] ): array { global $wpdb; $in = implode( ',', array_fill( 0, count( $statuses ), '%s' ) ); return array_map( 'intval', (array) $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_status IN ($in) ORDER BY ID ASC", array_merge( [ $type ], $statuses ) ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- placeholders list. } private function count( string $type ): int { global $wpdb; return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status NOT IN ('trash','auto-draft')", $type ) ); } }