Plugin Directory


Ignore:
Timestamp:
11/08/2026 11:23:25 AM (60 minutes ago)
Author:
rentbaik
Message:

Release 1.0.1

File:
1 edited

Legend:

Unmodified
Added
Removed
  • rentbaik/trunk/includes/Setup/SampleData.php

    r3790685 r3790901  
    11<?php
    22/**
    3  * Sample data: example vehicles, units, extras and bookings for a first look, removable in one click.
     3 * Sample data: example vehicles, units, extras and a month of bookings for a first look, removable in one click.
    44 *
    55 * @package Rentbaik
    … …  
    88namespace Rentbaik\Setup;
    99
     10use DateTimeImmutable;
    1011use Rentbaik\Availability\BlockRepository;
    1112use Rentbaik\Booking\BookingRepository;
    1213use Rentbaik\Booking\BookingService;
    1314use Rentbaik\Customers\CustomerRepository;
     15use Rentbaik\Customers\Photos;
    1416use Rentbaik\Database\Repository;
    1517use Rentbaik\Fleet\UnitRepository;
    … …  
    3638
    3739        /**
    38          * Post meta that marks a sample vehicle.
     40         * Post meta that marks a sample vehicle (and sample images in the Media Library).
    3941         */
    4042        public const META = '_rentbaik_sample';
    … …  
    8890
    8991        /**
     92         * Payment methods paid before the pick-up (the others are paid at the counter).
     93         */
     94        private const PREPAID = array( 'bank_transfer', 'woocommerce', 'qris' );
     95
     96        /**
     97         * Media Library images made in this request: asset name => attachment ID.
     98         *
     99         * @var array<string, int>
     100         */
     101        private static $media = array();
     102
     103        /**
    90104         * Saved record of the sample data.
    91105         *
    … …  
    101115                        'bookings'  => array(),
    102116                        'customers' => array(),
     117                        'media'     => array(),
     118                        'blocks'    => array(),
    103119                );
    104120                foreach ( $defaults as $key => $empty ) {
    … …  
    144160                $types    = array_keys( Vehicle::enabled_types() );
    145161                $plate    = 100;
    146                 $first    = array();
     162                $models   = array();
     163                $extras   = array();
    147164
    148165                Emails::mute( true );
    … …  
    157174                                }
    158175                                $data['vehicles'][] = $vehicle_id;
    159                                 if ( ! isset( $first[ $definition['type'] ] ) ) {
    160                                         $first[ $definition['type'] ] = $vehicle_id;
    161                                 }
     176                                self::add_vehicle_images( $vehicle_id, $definition );
     177                                $models[ $definition['art'] ] = $vehicle_id;
    162178                                for ( $i = 0; $i < $definition['units']; $i++ ) {
    163179                                        $unit_id = self::create_unit( $vehicle_id, $location, $plate, $definition['colors'][ $i % count( $definition['colors'] ) ] );
    … …  
    165181                                        if ( $unit_id > 0 ) {
    166182                                                $data['units'][] = $unit_id;
     183                                                // A photo of this unit in its own colour.
     184                                                $art_color = $definition['art_colors'][ $i % count( $definition['art_colors'] ) ];
     185                                                $photo     = self::media( $definition['art'] . '-' . $art_color, (string) $definition['title'] );
     186                                                if ( $photo > 0 ) {
     187                                                        UnitRepository::save( $unit_id, array( 'photo_id' => $photo ) );
     188                                                }
    167189                                        }
    168190                                }
    169191                        }
    170192
    171                         foreach ( self::extra_definitions() as $extra ) {
     193                        foreach ( self::extra_definitions() as $key => $extra ) {
    172194                                if ( empty( array_intersect( $extra['vehicle_types'], $types ) ) ) {
    173195                                        continue;
    … …  
    190212                                if ( $extra_id > 0 ) {
    191213                                        $data['extras'][] = $extra_id;
     214                                        $extras[ $key ]   = $extra_id;
    192215                                }
    193216                        }
    194217
    195218                        // Save what exists so far, so a failure below still leaves everything removable.
     219                        $data['media'] = array_values( array_unique( array_merge( $data['media'], array_values( self::$media ) ) ) );
    196220                        update_option( self::OPTION, $data, false );
    197221
    198                         $vehicles = array_values( $first );
    199                         if ( ! empty( $vehicles ) ) {
    200                                 $data = self::create_bookings( $data, $vehicles, $location );
     222                        if ( ! empty( $models ) ) {
     223                                $data = self::create_bookings( $data, $models, $extras, $location );
     224                                update_option( self::OPTION, $data, false );
     225                                $data = self::add_service_block( $data, $models );
    201226                        }
    202227                } finally {
    … …  
    204229                }
    205230
     231                $data['media'] = array_values( array_unique( array_merge( $data['media'], array_values( self::$media ) ) ) );
    206232                update_option( self::OPTION, $data, false );
    207233                Repository::bump_data_version();
    … …  
    222248        public static function remove(): void {
    223249                $data = self::data();
     250
     251                foreach ( $data['blocks'] as $block_id ) {
     252                        BlockRepository::delete( $block_id );
     253                }
    224254
    225255                foreach ( $data['bookings'] as $booking_id ) {
    … …  
    233263                foreach ( $data['customers'] as $customer_id ) {
    234264                        if ( empty( BookingRepository::for_customer( $customer_id ) ) ) {
     265                                // Sample photos and payment proofs in the private folder.
     266                                Photos::remove_for_customer( $customer_id );
    235267                                CustomerRepository::delete( $customer_id );
    236268                        }
    … …  
    270302                }
    271303
     304                foreach ( $data['media'] as $attachment_id ) {
     305                        if ( '1' === (string) get_post_meta( $attachment_id, self::META, true ) ) {
     306                                wp_delete_attachment( $attachment_id, true );
     307                        }
     308                }
     309
    272310                delete_option( self::OPTION );
    273311                Repository::bump_data_version();
     312        }
     313
     314        /**
     315         * Path of a sample illustration shipped with the plugin.
     316         *
     317         * @param string $name Asset name without extension.
     318         * @return string '' when missing.
     319         */
     320        private static function asset( string $name ): string {
     321                $path = RENTBAIK_PATH . 'assets/sample/' . sanitize_file_name( $name ) . '.png';
     322                return file_exists( $path ) ? $path : '';
     323        }
     324
     325        /**
     326         * Media Library image from a sample illustration (made once per request and reused).
     327         *
     328         * @param string $name  Asset name.
     329         * @param string $title Image title.
     330         * @return int Attachment ID (0 when it could not be made).
     331         */
     332        private static function media( string $name, string $title ): int {
     333                if ( isset( self::$media[ $name ] ) ) {
     334                        return self::$media[ $name ];
     335                }
     336                $source = self::asset( $name );
     337                if ( '' === $source ) {
     338                        return 0;
     339                }
     340                $upload = wp_upload_bits( 'rentbaik-sample-' . $name . '.png', null, (string) Photos::filesystem()->get_contents( $source ) );
     341                if ( ! empty( $upload['error'] ) ) {
     342                        return 0;
     343                }
     344                $attachment_id = wp_insert_attachment(
     345                        array(
     346                                'post_mime_type' => 'image/png',
     347                                'post_title'     => $title,
     348                                'post_status'    => 'inherit',
     349                        ),
     350                        $upload['file']
     351                );
     352                if ( ! $attachment_id || is_wp_error( $attachment_id ) ) {
     353                        return 0;
     354                }
     355                require_once ABSPATH . 'wp-admin/includes/image.php';
     356                wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) );
     357                update_post_meta( $attachment_id, self::META, '1' );
     358                update_post_meta( $attachment_id, '_wp_attachment_image_alt', $title );
     359                self::$media[ $name ] = (int) $attachment_id;
     360                return (int) $attachment_id;
     361        }
     362
     363        /**
     364         * Featured image and gallery of a sample vehicle: its colours and the gear that comes with it.
     365         *
     366         * @param int                  $vehicle_id Vehicle post ID.
     367         * @param array<string, mixed> $definition Vehicle definition.
     368         * @return void
     369         */
     370        private static function add_vehicle_images( int $vehicle_id, array $definition ): void {
     371                $title    = (string) $definition['title'];
     372                $featured = self::media( $definition['art'] . '-' . $definition['art_colors'][0], $title );
     373                if ( $featured > 0 ) {
     374                        set_post_thumbnail( $vehicle_id, $featured );
     375                }
     376                $gallery = array();
     377                foreach ( array_slice( $definition['art_colors'], 1 ) as $color ) {
     378                        $gallery[] = self::media( $definition['art'] . '-' . $color, $title );
     379                }
     380                $gear      = array(
     381                        'motorcycle' => 'gear-scooter',
     382                        'car'        => 'gear-car',
     383                );
     384                $gallery[] = self::media( $gear[ $definition['type'] ] ?? 'gear-bike', __( 'Included gear (sample)', 'rentbaik' ) );
     385                $gallery   = array_filter( $gallery );
     386                if ( ! empty( $gallery ) ) {
     387                        update_post_meta( $vehicle_id, '_rentbaik_gallery', implode( ',', $gallery ) );
     388                }
     389        }
     390
     391        /**
     392         * Copy a sample illustration into the private folder.
     393         *
     394         * @param string $name   Asset name.
     395         * @param string $prefix File name prefix (photo, proof).
     396         * @return string File name in the private folder ('' when missing).
     397         */
     398        private static function private_copy( string $name, string $prefix ): string {
     399                $source = self::asset( $name );
     400                if ( '' === $source ) {
     401                        return '';
     402                }
     403                $file = $prefix . '-' . strtolower( wp_generate_password( 24, false ) ) . '.png';
     404                $path = Photos::dir() . '/' . $file;
     405                if ( ! Photos::filesystem()->put_contents( $path, (string) Photos::filesystem()->get_contents( $source ), FS_CHMOD_FILE ) ) {
     406                        return '';
     407                }
     408                Photos::make_thumb( $path );
     409                return $file;
     410        }
     411
     412        /**
     413         * Customer photo taken at the hand-over and the transfer receipt a customer uploaded, as on a real booking.
     414         *
     415         * @param int                  $booking_id Booking ID.
     416         * @param array<string, mixed> $plan       Plan.
     417         * @return void
     418         */
     419        private static function add_booking_files( int $booking_id, array $plan ): void {
     420                $booking = BookingRepository::find( $booking_id );
     421                if ( ! $booking ) {
     422                        return;
     423                }
     424                $add = static function ( string $asset, string $prefix, string $type, string $status, string $when ) use ( $booking_id, $booking ): void {
     425                        $file = self::private_copy( $asset, $prefix );
     426                        if ( '' !== $file ) {
     427                                Photos::insert(
     428                                        array(
     429                                                'customer_id' => (int) $booking['customer_id'],
     430                                                'booking_id'  => $booking_id,
     431                                                'doc_type'    => $type,
     432                                                'file_name'   => $file,
     433                                                'mime_type'   => 'image/png',
     434                                                'status'      => $status,
     435                                                'created_at'  => $when,
     436                                        ),
     437                                        array( '%d', '%d', '%s', '%s', '%s', '%s', '%s' )
     438                                );
     439                        }
     440                };
     441                if ( ! empty( $plan['proof'] ) ) {
     442                        $add( 'receipt-sample', 'proof', 'payment_proof', (string) $plan['proof'], self::created_at( $booking ) );
     443                }
     444                if ( ! empty( $plan['photo'] ) && in_array( $plan['status'], array( 'active', 'returned', 'completed' ), true ) ) {
     445                        $add( 'customer-' . (int) $plan['photo'], 'photo', 'photo', 'valid', (string) $booking['pickup_at'] );
     446                }
    274447        }
    275448
    … …  
    283456                return array(
    284457                        array(
    285                                 'type'    => 'motorcycle',
    286                                 'title'   => __( 'Automatic scooter 125 cc (sample)', 'rentbaik' ),
    287                                 'content' => __( 'Easy automatic scooter for the city. Two helmets included.', 'rentbaik' ) . "\n\n" . $note,
    288                                 'specs'   => array(
     458                                'type'       => 'motorcycle',
     459                                'art'        => 'scooter-125',
     460                                'art_colors' => array( 'white', 'red', 'black' ),
     461                                'title'      => __( 'Automatic scooter 125 cc (sample)', 'rentbaik' ),
     462                                'content'    => __( 'Easy automatic scooter for the city. Two helmets included.', 'rentbaik' ) . "\n\n" . $note,
     463                                'specs'      => array(
    289464                                        'engine_cc'    => 125,
    290465                                        'transmission' => 'automatic',
    291466                                        'helmets'      => 2,
    292467                                ),
    293                                 'rates'   => array(
     468                                'rates'      => array(
    294469                                        'hour' => 4,
    295470                                        'day'  => 15,
    296471                                        'week' => 90,
    297472                                ),
    298                                 'deposit' => 50,
    299                                 'units'   => 3,
    300                                 'colors'  => array( __( 'White', 'rentbaik' ), __( 'Red', 'rentbaik' ), __( 'Black', 'rentbaik' ) ),
     473                                'deposit'    => 50,
     474                                'units'      => 3,
     475                                'colors'     => array( __( 'White', 'rentbaik' ), __( 'Red', 'rentbaik' ), __( 'Black', 'rentbaik' ) ),
    301476                        ),
    302477                        array(
    303                                 'type'    => 'motorcycle',
    304                                 'title'   => __( 'Comfort scooter 160 cc (sample)', 'rentbaik' ),
    305                                 'content' => __( 'Bigger scooter with a large seat and storage, good for longer trips.', 'rentbaik' ) . "\n\n" . $note,
    306                                 'specs'   => array(
     478                                'type'       => 'motorcycle',
     479                                'art'        => 'scooter-160',
     480                                'art_colors' => array( 'grey', 'blue' ),
     481                                'title'      => __( 'Comfort scooter 160 cc (sample)', 'rentbaik' ),
     482                                'content'    => __( 'Bigger scooter with a large seat and storage, good for longer trips.', 'rentbaik' ) . "\n\n" . $note,
     483                                'specs'      => array(
    307484                                        'engine_cc'    => 160,
    308485                                        'transmission' => 'automatic',
    309486                                        'helmets'      => 2,
    310487                                ),
    311                                 'rates'   => array(
     488                                'rates'      => array(
    312489                                        'day'  => 22,
    313490                                        'week' => 130,
    314491                                ),
    315                                 'deposit' => 80,
    316                                 'units'   => 2,
    317                                 'colors'  => array( __( 'Grey', 'rentbaik' ), __( 'Blue', 'rentbaik' ) ),
     492                                'deposit'    => 80,
     493                                'units'      => 2,
     494                                'colors'     => array( __( 'Grey', 'rentbaik' ), __( 'Blue', 'rentbaik' ) ),
    318495                        ),
    319496                        array(
    320                                 'type'    => 'car',
    321                                 'title'   => __( 'Compact car, automatic (sample)', 'rentbaik' ),
    322                                 'content' => __( 'Small and easy to park. Air conditioning, five seats.', 'rentbaik' ) . "\n\n" . $note,
    323                                 'specs'   => array(
     497                                'type'       => 'car',
     498                                'art'        => 'car-compact',
     499                                'art_colors' => array( 'silver', 'white' ),
     500                                'title'      => __( 'Compact car, automatic (sample)', 'rentbaik' ),
     501                                'content'    => __( 'Small and easy to park. Air conditioning, five seats.', 'rentbaik' ) . "\n\n" . $note,
     502                                'specs'      => array(
    324503                                        'seats'        => 5,
    325504                                        'doors'        => 5,
    … …  
    329508                                        'luggage'      => 2,
    330509                                ),
    331                                 'rates'   => array(
     510                                'rates'      => array(
    332511                                        'day'  => 45,
    333512                                        'week' => 270,
    334513                                ),
    335                                 'deposit' => 200,
    336                                 'units'   => 2,
    337                                 'colors'  => array( __( 'Silver', 'rentbaik' ), __( 'White', 'rentbaik' ) ),
     514                                'deposit'    => 200,
     515                                'units'      => 2,
     516                                'colors'     => array( __( 'Silver', 'rentbaik' ), __( 'White', 'rentbaik' ) ),
    338517                        ),
    339518                        array(
    340                                 'type'    => 'car',
    341                                 'title'   => __( 'Family car, 7 seats (sample)', 'rentbaik' ),
    342                                 'content' => __( 'Room for the whole family and their bags.', 'rentbaik' ) . "\n\n" . $note,
    343                                 'specs'   => array(
     519                                'type'       => 'car',
     520                                'art'        => 'car-family',
     521                                'art_colors' => array( 'black' ),
     522                                'title'      => __( 'Family car, 7 seats (sample)', 'rentbaik' ),
     523                                'content'    => __( 'Room for the whole family and their bags.', 'rentbaik' ) . "\n\n" . $note,
     524                                'specs'      => array(
    344525                                        'seats'        => 7,
    345526                                        'doors'        => 5,
    … …  
    349530                                        'luggage'      => 3,
    350531                                ),
    351                                 'rates'   => array(
     532                                'rates'      => array(
    352533                                        'day'  => 65,
    353534                                        'week' => 390,
    354535                                ),
    355                                 'deposit' => 300,
    356                                 'units'   => 1,
    357                                 'colors'  => array( __( 'Black', 'rentbaik' ) ),
     536                                'deposit'    => 300,
     537                                'units'      => 1,
     538                                'colors'     => array( __( 'Black', 'rentbaik' ) ),
    358539                        ),
    359540                        array(
    360                                 'type'    => 'bicycle',
    361                                 'title'   => __( 'City bike (sample)', 'rentbaik' ),
    362                                 'content' => __( 'Comfortable bike with a basket and a lock.', 'rentbaik' ) . "\n\n" . $note,
    363                                 'specs'   => array(
     541                                'type'       => 'bicycle',
     542                                'art'        => 'bike-city',
     543                                'art_colors' => array( 'green' ),
     544                                'title'      => __( 'City bike (sample)', 'rentbaik' ),
     545                                'content'    => __( 'Comfortable bike with a basket and a lock.', 'rentbaik' ) . "\n\n" . $note,
     546                                'specs'      => array(
    364547                                        'frame_size' => 'M',
    365548                                        'gears'      => 7,
    366549                                ),
    367                                 'rates'   => array(
     550                                'rates'      => array(
    368551                                        'hour' => 3,
    369552                                        'day'  => 10,
    370553                                ),
    371                                 'deposit' => 0,
    372                                 'units'   => 2,
    373                                 'colors'  => array( __( 'Green', 'rentbaik' ) ),
     554                                'deposit'    => 0,
     555                                'units'      => 2,
     556                                'colors'     => array( __( 'Green', 'rentbaik' ) ),
    374557                        ),
    375558                        array(
    376                                 'type'    => 'ebike',
    377                                 'title'   => __( 'Electric bike (sample)', 'rentbaik' ),
    378                                 'content' => __( 'Pedal assist up to 60 km per charge.', 'rentbaik' ) . "\n\n" . $note,
    379                                 'specs'   => array(
     559                                'type'       => 'ebike',
     560                                'art'        => 'ebike',
     561                                'art_colors' => array( 'teal' ),
     562                                'title'      => __( 'Electric bike (sample)', 'rentbaik' ),
     563                                'content'    => __( 'Pedal assist up to 60 km per charge.', 'rentbaik' ) . "\n\n" . $note,
     564                                'specs'      => array(
    380565                                        'frame_size' => 'M',
    381566                                        'range_km'   => 60,
    382567                                ),
    383                                 'rates'   => array(
     568                                'rates'      => array(
    384569                                        'hour' => 6,
    385570                                        'day'  => 25,
    386571                                ),
    387                                 'deposit' => 100,
    388                                 'units'   => 2,
    389                                 'colors'  => array( __( 'White', 'rentbaik' ) ),
    390                         ),
    391                 );
    392         }
    393 
    394         /**
    395          * Sample extras.
    396          *
    397          * @return array<int, array<string, mixed>>
     572                                'deposit'    => 100,
     573                                'units'      => 2,
     574                                'colors'     => array( __( 'White', 'rentbaik' ) ),
     575                        ),
     576                );
     577        }
     578
     579        /**
     580         * Sample extras, by key.
     581         *
     582         * @return array<string, array<string, mixed>>
    398583         */
    399584        private static function extra_definitions(): array {
    400585                return array(
    401                         array(
     586                        'helmet'     => array(
    402587                                'name'          => __( 'Extra helmet (sample)', 'rentbaik' ),
    403588                                'description'   => __( 'Clean helmet for a passenger.', 'rentbaik' ),
    … …  
    407592                                'vehicle_types' => array( 'motorcycle', 'bicycle', 'ebike' ),
    408593                        ),
    409                         array(
     594                        'poncho'     => array(
    410595                                'name'          => __( 'Rain poncho (sample)', 'rentbaik' ),
    411596                                'description'   => __( 'Yours to keep.', 'rentbaik' ),
    … …  
    415600                                'vehicle_types' => array( 'motorcycle', 'bicycle', 'ebike' ),
    416601                        ),
    417                         array(
     602                        'child_seat' => array(
    418603                                'name'          => __( 'Child seat (sample)', 'rentbaik' ),
    419604                                'description'   => __( 'For children from 9 to 18 kg.', 'rentbaik' ),
    … …  
    490675
    491676        /**
    492          * Three sample bookings: one picked up today, one returning today and one waiting for confirmation.
     677         * A month of sample bookings in every common situation, around today.
     678         *
     679         * Keys: model (vehicle art), offset (days from today), hour, days, status, source, method (payment), paid
     680         * (none, part, full, refunded), part (share paid), deposit (none, held, returned), proof (pending, valid),
     681         * photo (customer portrait 1-8), extra (extra key), no_unit (taken although no unit is free).
     682         *
     683         * @return array<int, array<string, mixed>>
     684         */
     685        private static function plans(): array {
     686                $keys = array( 'first_name', 'country', 'phone', 'model', 'offset', 'hour', 'days', 'status', 'source', 'method', 'paid', 'part', 'deposit', 'proof', 'photo', 'extra', 'no_unit' );
     687                $rows = array(
     688                        // Finished last week: paid in cash, deposit given back.
     689                        array( 'Emma', 'GB', '15550106', 'scooter-125', -7, 10, 2, 'completed', 'whatsapp', 'cash', 'full', 0, 'returned', '', 1, 'helmet', false ),
     690                        // Finished: paid by card at the counter.
     691                        array( 'Olivia', 'US', '15550108', 'car-compact', -6, 9, 2, 'completed', 'web', 'card', 'full', 0, 'returned', '', 4, 'child_seat', false ),
     692                        // Returned two days ago, the deposit is still to be given back.
     693                        array( 'Lucas', 'FR', '15550103', 'scooter-160', -4, 9, 2, 'returned', 'admin', 'cash', 'full', 0, 'held', '', 3, '', false ),
     694                        // Cancelled by the customer: the transfer was refunded.
     695                        array( 'Liam', 'NZ', '15550107', 'bike-city', -3, 10, 1, 'cancelled', 'web', 'bank_transfer', 'refunded', 0.5, 'none', '', 0, '', false ),
     696                        // Did not come.
     697                        array( 'Wei', 'CN', '15550109', 'ebike', -2, 11, 1, 'no_show', 'web', 'pay_at_pickup', 'none', 0, 'none', '', 0, '', false ),
     698                        // Should have come back yesterday: late, deposit held, rent not paid yet.
     699                        array( 'Noah', 'US', '15550105', 'car-compact', -3, 9, 2, 'active', 'admin', 'pay_at_pickup', 'none', 0, 'held', '', 7, '', false ),
     700                        // On rent, comes back today; half paid with a QR code.
     701                        array( 'Maria', 'DE', '15550101', 'scooter-125', -2, 16, 2, 'active', 'whatsapp', 'qris', 'part', 0.5, 'held', '', 2, 'poncho', false ),
     702                        // Long rental under way, paid in advance by bank transfer.
     703                        array( 'Sofia', 'IT', '15550104', 'car-family', -5, 10, 12, 'active', 'web', 'bank_transfer', 'full', 0, 'held', 'valid', 5, '', false ),
     704                        // Picks up today, pays at the counter.
     705                        array( 'Alex', 'AU', '15550100', 'scooter-125', 0, 10, 3, 'confirmed', 'web', 'pay_at_pickup', 'none', 0, 'none', '', 0, '', false ),
     706                        // Picks up this afternoon, already paid online.
     707                        array( 'Aisha', 'AE', '15550110', 'scooter-160', 0, 15, 2, 'confirmed', 'web', 'woocommerce', 'full', 0, 'none', '', 0, '', false ),
     708                        // Taken by phone although the only family car is out: needs a unit before pick-up.
     709                        array( 'Omar', 'EG', '15550111', 'car-family', 2, 10, 3, 'confirmed', 'admin', 'pay_at_pickup', 'none', 0, 'none', '', 0, '', true ),
     710                        // Waiting for confirmation, transfer receipt uploaded but not checked yet.
     711                        array( 'Kenji', 'JP', '15550102', 'ebike', 3, 11, 4, 'pending', 'web', 'bank_transfer', 'none', 0, 'none', 'pending', 0, '', false ),
     712                        // Confirmed with a part payment by transfer.
     713                        array( 'Priya', 'IN', '15550112', 'car-compact', 6, 10, 5, 'confirmed', 'web', 'bank_transfer', 'part', 0.3, 'none', 'valid', 0, 'child_seat', false ),
     714                        // A week by WhatsApp, pays at pick-up.
     715                        array( 'Mateo', 'MX', '15550113', 'scooter-125', 9, 9, 7, 'confirmed', 'whatsapp', 'pay_at_pickup', 'none', 0, 'none', '', 0, 'helmet', false ),
     716                        // Waiting for confirmation from WhatsApp.
     717                        array( 'Putri', 'ID', '15550114', 'bike-city', 12, 8, 1, 'pending', 'whatsapp', 'pay_at_pickup', 'none', 0, 'none', '', 0, '', false ),
     718                        // Paid online for later this month.
     719                        array( 'Lena', 'SE', '15550115', 'car-compact', 16, 10, 4, 'confirmed', 'web', 'woocommerce', 'full', 0, 'none', '', 0, '', false ),
     720                        // A returning customer books again.
     721                        array( 'Emma', 'GB', '15550106', 'scooter-160', 19, 10, 3, 'pending', 'whatsapp', 'pay_at_pickup', 'none', 0, 'none', '', 0, '', false ),
     722                );
     723                return array_map(
     724                        static function ( array $row ) use ( $keys ): array {
     725                                return array_combine( $keys, $row );
     726                        },
     727                        $rows
     728                );
     729        }
     730
     731        /**
     732         * The planned model, or a similar one when its vehicle type is switched off (a shop with only cars still gets
     733         * every situation).
     734         *
     735         * @param string             $model  Vehicle art.
     736         * @param array<string, int> $models Created models: art => vehicle ID.
     737         * @return string '' when no model exists.
     738         */
     739        private static function model_for( string $model, array $models ): string {
     740                $similar = array(
     741                        'scooter-125' => array( 'scooter-160', 'bike-city', 'ebike' ),
     742                        'scooter-160' => array( 'scooter-125', 'ebike', 'bike-city' ),
     743                        'car-compact' => array( 'car-family', 'scooter-160', 'scooter-125' ),
     744                        'car-family'  => array( 'car-compact', 'scooter-160', 'scooter-125' ),
     745                        'bike-city'   => array( 'ebike', 'scooter-125', 'scooter-160' ),
     746                        'ebike'       => array( 'bike-city', 'scooter-160', 'scooter-125' ),
     747                );
     748                foreach ( array_merge( array( $model ), $similar[ $model ] ?? array(), array_keys( $models ) ) as $candidate ) {
     749                        if ( isset( $models[ $candidate ] ) ) {
     750                                return $candidate;
     751                        }
     752                }
     753                return '';
     754        }
     755
     756        /**
     757         * Days from today for a plan, kept inside the current month where its situation allows it.
     758         *
     759         * @param array<string, mixed> $plan Plan.
     760         * @return int
     761         */
     762        private static function offset_in_month( array $plan ): int {
     763                $today  = new DateTimeImmutable( 'today', wp_timezone() );
     764                $offset = (int) $plan['offset'];
     765                $days   = (int) $plan['days'];
     766                if ( $offset > 0 ) {
     767                        // Later this month: keep the whole rental before the end of the month when there is room.
     768                        $left = (int) $today->format( 't' ) - (int) $today->format( 'j' );
     769                        return max( 1, min( $offset, $left - $days ) );
     770                }
     771                if ( $offset + $days <= 0 ) {
     772                        // Already over: as early as needed, but not before the first day of the month when it fits.
     773                        $first = 1 - (int) $today->format( 'j' );
     774                        return max( $offset, $first ) + $days <= 0 ? max( $offset, $first ) : $offset;
     775                }
     776                return $offset;
     777        }
     778
     779        /**
     780         * Create the sample bookings and bring each to the situation of its plan.
    493781         *
    494782         * @param array<string, int[]> $data        Sample data record.
    495          * @param int[]                $vehicles    Sample vehicle IDs to book.
     783         * @param array<string, int>   $models      Vehicle art => vehicle ID.
     784         * @param array<string, int>   $extras      Extra key => extra ID.
    496785         * @param int                  $location_id Location ID.
    497786         * @return array<string, int[]>
    498787         */
    499         private static function create_bookings( array $data, array $vehicles, int $location_id ): array {
    500                 $plans = array(
    501                         array(
    502                                 'first_name' => 'Alex',
    503                                 'country'    => 'AU',
    504                                 'phone'      => '15550100',
    505                                 'offset'     => 0,
    506                                 'hour'       => 10,
    507                                 'days'       => 3,
    508                                 'status'     => 'confirmed',
    509                                 'source'     => 'web',
    510                         ),
    511                         array(
    512                                 'first_name' => 'Maria',
    513                                 'country'    => 'DE',
    514                                 'phone'      => '15550101',
    515                                 'offset'     => -2,
    516                                 'hour'       => 16,
    517                                 'days'       => 2,
    518                                 'status'     => 'active',
    519                                 'source'     => 'whatsapp',
    520                         ),
    521                         array(
    522                                 'first_name' => 'Kenji',
    523                                 'country'    => 'JP',
    524                                 'phone'      => '15550102',
    525                                 'offset'     => 3,
    526                                 'hour'       => 11,
    527                                 'days'       => 4,
    528                                 'status'     => 'pending',
    529                                 'source'     => 'web',
    530                         ),
    531                 );
    532 
    533                 foreach ( $plans as $index => $plan ) {
    534                         $vehicle_id = $vehicles[ $index % count( $vehicles ) ];
     788        private static function create_bookings( array $data, array $models, array $extras, int $location_id ): array {
     789                foreach ( self::plans() as $plan ) {
     790                        $plan['model'] = self::model_for( (string) $plan['model'], $models );
     791                        if ( '' === $plan['model'] ) {
     792                                continue;
     793                        }
     794                        $offset     = self::offset_in_month( $plan );
    535795                        $booking_id = 0;
    536                         // Move a day later when the location is closed on the planned day.
     796                        // Move a day later when the location is closed on the planned day or no unit is free.
    537797                        for ( $shift = 0; $shift < 7 && $booking_id <= 0; $shift++ ) {
    538                                 $pickup     = Time::local_midnight( $plan['offset'] + $shift )->modify( '+' . $plan['hour'] . ' hours' );
    539                                 $result     = BookingService::create(
    540                                         array(
    541                                                 'vehicle_id'     => $vehicle_id,
    542                                                 'pickup'         => $pickup,
    543                                                 'return'         => $pickup->modify( '+' . $plan['days'] . ' days' ),
    544                                                 'location_id'    => $location_id,
    545                                                 'customer'       => array(
    546                                                         'first_name' => $plan['first_name'],
    547                                                         'last_name'  => __( 'Sample', 'rentbaik' ),
    548                                                         'phone'      => $plan['phone'],
    549                                                         'country'    => $plan['country'],
    550                                                         'notes'      => __( 'Sample data', 'rentbaik' ),
    551                                                 ),
    552                                                 'status'         => 'pending' === $plan['status'] ? 'pending' : 'confirmed',
    553                                                 'source'         => $plan['source'],
    554                                                 'payment_method' => 'pay_at_pickup',
    555                                                 'internal_note'  => __( 'Sample booking. It is removed together with the sample data.', 'rentbaik' ),
    556                                                 'allow_past'     => true,
    557                                         )
     798                                $pickup = Time::local_midnight( $offset + $shift )->modify( '+' . $plan['hour'] . ' hours' );
     799                                $input  = array(
     800                                        'vehicle_id'       => $models[ $plan['model'] ],
     801                                        'pickup'           => $pickup,
     802                                        'return'           => $pickup->modify( '+' . $plan['days'] . ' days' ),
     803                                        'location_id'      => $location_id,
     804                                        'customer'         => array(
     805                                                'first_name' => $plan['first_name'],
     806                                                'last_name'  => __( 'Sample', 'rentbaik' ),
     807                                                'phone'      => $plan['phone'],
     808                                                'country'    => $plan['country'],
     809                                                'notes'      => __( 'Sample data', 'rentbaik' ),
     810                                        ),
     811                                        'status'           => 'pending' === $plan['status'] ? 'pending' : 'confirmed',
     812                                        'source'           => $plan['source'],
     813                                        'payment_method'   => $plan['method'],
     814                                        'internal_note'    => __( 'Sample booking. It is removed together with the sample data.', 'rentbaik' ),
     815                                        'allow_past'       => true,
     816                                        'allow_unassigned' => ! empty( $plan['no_unit'] ),
    558817                                );
     818                                if ( ! empty( $plan['extra'] ) && isset( $extras[ $plan['extra'] ] ) ) {
     819                                        $input['extras'] = array( $extras[ $plan['extra'] ] => 1 );
     820                                }
     821                                $result = BookingService::create( $input );
     822                                // An extra the vehicle type does not allow: try again without it.
     823                                if ( ! is_int( $result ) && isset( $input['extras'] ) ) {
     824                                        unset( $input['extras'] );
     825                                        $result = BookingService::create( $input );
     826                                }
    559827                                $booking_id = is_int( $result ) ? $result : 0;
    560828                        }
    … …  
    567835                                $data['customers'][] = (int) $booking['customer_id'];
    568836                        }
    569                         if ( 'active' === $plan['status'] ) {
    570                                 BookingService::set_status( $booking_id, 'active' );
     837                        self::play_out( $booking_id, $plan );
     838                        self::add_booking_files( $booking_id, $plan );
     839                }
     840                return $data;
     841        }
     842
     843        /**
     844         * When the sample booking was made: three days before the pick-up, never later than an hour ago.
     845         *
     846         * @param array<string, mixed> $booking Booking row.
     847         * @return string UTC (Y-m-d H:i:s).
     848         */
     849        private static function created_at( array $booking ): string {
     850                $pickup = Time::from_utc( (string) $booking['pickup_at'] );
     851                $latest = Time::now()->modify( '-1 hour' );
     852                $made   = $pickup ? $pickup->modify( '-3 days' ) : $latest;
     853                return Time::to_mysql( $made < $latest ? $made : $latest );
     854        }
     855
     856        /**
     857         * Bring a sample booking to the situation of its plan: handed over, paid, returned, cancelled or not shown.
     858         * Dates are set to when it would have happened, so the reports and the calendar look like a real month.
     859         *
     860         * @param int                  $booking_id Booking ID.
     861         * @param array<string, mixed> $plan       Plan.
     862         * @return void
     863         */
     864        private static function play_out( int $booking_id, array $plan ): void {
     865                $booking = BookingRepository::find( $booking_id );
     866                if ( ! $booking ) {
     867                        return;
     868                }
     869                $now     = Time::to_mysql( Time::now() );
     870                $made    = self::created_at( $booking );
     871                $pickup  = min( (string) $booking['pickup_at'], $now );
     872                $return  = min( (string) $booking['return_at'], $now );
     873                $method  = (string) $plan['method'];
     874                $paid    = (string) ( $plan['paid'] ?? 'none' );
     875                $deposit = (string) ( $plan['deposit'] ?? 'none' );
     876                // Money paid in advance arrives two hours after booking; the rest at the counter.
     877                $prepaid = Time::to_mysql( min( Time::now(), Time::from_utc( $made )->modify( '+2 hours' ) ) );
     878                $pay     = static function ( string $type, string $how, int $amount, string $when, string $note = '' ) use ( $booking_id ): void {
     879                        if ( $amount > 0 ) {
     880                                BookingService::add_payment(
     881                                        $booking_id,
     882                                        array(
     883                                                'payment_type' => $type,
     884                                                'method'       => $how,
     885                                                'amount'       => $amount,
     886                                                'paid_at'      => $when,
     887                                                'reference'    => 'cash' === $how ? '' : 'SAMPLE-' . $booking_id,
     888                                                'note'         => '' === $note ? __( 'Sample payment', 'rentbaik' ) : $note,
     889                                        )
     890                                );
     891                        }
     892                };
     893
     894                BookingRepository::patch( $booking_id, array( 'created_at' => $made ) );
     895
     896                $total = (int) $booking['total'];
     897                $share = 'full' === $paid ? $total : (int) round( $total * (float) ( $plan['part'] ?? 0 ) );
     898                if ( in_array( $paid, array( 'full', 'part', 'refunded' ), true ) && in_array( $method, self::PREPAID, true ) ) {
     899                        $pay( 'payment', $method, $share, $prepaid );
     900                }
     901
     902                if ( in_array( $plan['status'], array( 'active', 'returned', 'completed' ), true ) ) {
     903                        BookingService::set_status( $booking_id, 'active' );
     904                        BookingRepository::patch( $booking_id, array( 'actual_pickup_at' => $pickup ) );
     905                        if ( in_array( $paid, array( 'full', 'part' ), true ) && ! in_array( $method, self::PREPAID, true ) ) {
     906                                $pay( 'payment', $method, $share, $pickup );
     907                        }
     908                        if ( 'none' !== $deposit ) {
     909                                $pay( 'deposit', 'cash', (int) $booking['deposit_amount'], $pickup, __( 'Deposit at pick-up', 'rentbaik' ) );
     910                        }
     911                }
     912
     913                if ( in_array( $plan['status'], array( 'returned', 'completed' ), true ) ) {
     914                        BookingService::set_status( $booking_id, 'returned' );
     915                        BookingRepository::patch( $booking_id, array( 'actual_return_at' => $return ) );
     916                        if ( 'returned' === $deposit ) {
     917                                $pay( 'deposit_refund', 'cash', (int) $booking['deposit_amount'], $return, __( 'Deposit given back', 'rentbaik' ) );
     918                        }
     919                        if ( 'completed' === $plan['status'] ) {
     920                                BookingService::set_status( $booking_id, 'completed' );
     921                        }
     922                }
     923
     924                if ( 'cancelled' === $plan['status'] ) {
     925                        BookingService::set_status( $booking_id, 'cancelled' );
     926                        if ( 'refunded' === $paid ) {
     927                                $pay( 'refund', $method, $share, max( $prepaid, Time::to_mysql( Time::now()->modify( '-1 day' ) ) ), __( 'Refund after cancelling', 'rentbaik' ) );
     928                        }
     929                }
     930                if ( 'no_show' === $plan['status'] ) {
     931                        BookingService::set_status( $booking_id, 'no_show' );
     932                }
     933        }
     934
     935        /**
     936         * Service for a scooter over the next two days, so the calendar shows a unit that cannot be rented.
     937         *
     938         * @param array<string, int[]> $data   Sample data record.
     939         * @param array<string, int>   $models Vehicle art => vehicle ID.
     940         * @return array<string, int[]>
     941         */
     942        private static function add_service_block( array $data, array $models ): array {
     943                $vehicle_id = $models['scooter-160'] ?? ( $models['scooter-125'] ?? 0 );
     944                if ( $vehicle_id <= 0 ) {
     945                        return $data;
     946                }
     947                $start = Time::to_mysql( Time::local_midnight( 1 )->modify( '+8 hours' ) );
     948                $end   = Time::to_mysql( Time::local_midnight( 3 )->modify( '+8 hours' ) );
     949                $units = array();
     950                foreach ( $data['units'] as $unit_id ) {
     951                        $unit = UnitRepository::find( $unit_id );
     952                        if ( $unit && (int) $unit['vehicle_id'] === $vehicle_id ) {
     953                                $units[] = $unit_id;
     954                        }
     955                }
     956                $busy = BlockRepository::busy_units( $units, $start, $end );
     957                foreach ( $units as $unit_id ) {
     958                        if ( ! in_array( $unit_id, $busy, true ) ) {
     959                                $block_id = BlockRepository::add( $unit_id, $start, $end, 'maintenance', 0, null, __( 'Oil change (sample)', 'rentbaik' ) );
     960                                if ( $block_id > 0 ) {
     961                                        $data['blocks'][] = $block_id;
     962                                }
     963                                break;
    571964                        }
    572965                }
    … …  
    580973         * @return int
    581974         */
    582         private static function price( float $usd ): int {
     975        public static function price( float $usd ): int {
    583976                if ( $usd <= 0 ) {
    584977                        return 0;
    … …  
    586979                $currency = Format::currency();
    587980                $amount   = $usd * (float) ( self::USD_RATES[ $currency ] ?? 1 );
    588                 $step     = $amount >= 100 ? 10 ** ( (int) floor( log10( $amount ) ) - 1 ) : 1;
    589                 $amount   = max( $step, round( $amount / $step ) * $step );
     981                // Small amounts keep halves (5.50), larger ones are rounded to two significant digits (88,000).
     982                $step   = $amount >= 100 ? 10 ** ( (int) floor( log10( $amount ) ) - 1 ) : ( $amount < 10 ? 0.5 : 1 );
     983                $amount = max( $step, round( $amount / $step ) * $step );
    590984                return (int) round( $amount * ( 10 ** Format::decimals() ) );
    591985        }
Note: See TracChangeset for help on using the changeset viewer.