Changeset 3790901 for rentbaik/trunk/includes/Setup/SampleData.php
- Timestamp:
- 11/08/2026 11:23:25 AM (60 minutes ago)
- File:
-
- 1 edited
-
rentbaik/trunk/includes/Setup/SampleData.php (modified) (22 diffs)
Legend:
- Unmodified
- Added
- Removed
-
rentbaik/trunk/includes/Setup/SampleData.php
r3790685 r3790901 1 1 <?php 2 2 /** 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. 4 4 * 5 5 * @package Rentbaik … … 8 8 namespace Rentbaik\Setup; 9 9 10 use DateTimeImmutable; 10 11 use Rentbaik\Availability\BlockRepository; 11 12 use Rentbaik\Booking\BookingRepository; 12 13 use Rentbaik\Booking\BookingService; 13 14 use Rentbaik\Customers\CustomerRepository; 15 use Rentbaik\Customers\Photos; 14 16 use Rentbaik\Database\Repository; 15 17 use Rentbaik\Fleet\UnitRepository; … … 36 38 37 39 /** 38 * Post meta that marks a sample vehicle .40 * Post meta that marks a sample vehicle (and sample images in the Media Library). 39 41 */ 40 42 public const META = '_rentbaik_sample'; … … 88 90 89 91 /** 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 /** 90 104 * Saved record of the sample data. 91 105 * … … 101 115 'bookings' => array(), 102 116 'customers' => array(), 117 'media' => array(), 118 'blocks' => array(), 103 119 ); 104 120 foreach ( $defaults as $key => $empty ) { … … 144 160 $types = array_keys( Vehicle::enabled_types() ); 145 161 $plate = 100; 146 $first = array(); 162 $models = array(); 163 $extras = array(); 147 164 148 165 Emails::mute( true ); … … 157 174 } 158 175 $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; 162 178 for ( $i = 0; $i < $definition['units']; $i++ ) { 163 179 $unit_id = self::create_unit( $vehicle_id, $location, $plate, $definition['colors'][ $i % count( $definition['colors'] ) ] ); … … 165 181 if ( $unit_id > 0 ) { 166 182 $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 } 167 189 } 168 190 } 169 191 } 170 192 171 foreach ( self::extra_definitions() as $ extra ) {193 foreach ( self::extra_definitions() as $key => $extra ) { 172 194 if ( empty( array_intersect( $extra['vehicle_types'], $types ) ) ) { 173 195 continue; … … 190 212 if ( $extra_id > 0 ) { 191 213 $data['extras'][] = $extra_id; 214 $extras[ $key ] = $extra_id; 192 215 } 193 216 } 194 217 195 218 // 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 ) ) ) ); 196 220 update_option( self::OPTION, $data, false ); 197 221 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 ); 201 226 } 202 227 } finally { … … 204 229 } 205 230 231 $data['media'] = array_values( array_unique( array_merge( $data['media'], array_values( self::$media ) ) ) ); 206 232 update_option( self::OPTION, $data, false ); 207 233 Repository::bump_data_version(); … … 222 248 public static function remove(): void { 223 249 $data = self::data(); 250 251 foreach ( $data['blocks'] as $block_id ) { 252 BlockRepository::delete( $block_id ); 253 } 224 254 225 255 foreach ( $data['bookings'] as $booking_id ) { … … 233 263 foreach ( $data['customers'] as $customer_id ) { 234 264 if ( empty( BookingRepository::for_customer( $customer_id ) ) ) { 265 // Sample photos and payment proofs in the private folder. 266 Photos::remove_for_customer( $customer_id ); 235 267 CustomerRepository::delete( $customer_id ); 236 268 } … … 270 302 } 271 303 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 272 310 delete_option( self::OPTION ); 273 311 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 } 274 447 } 275 448 … … 283 456 return array( 284 457 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( 289 464 'engine_cc' => 125, 290 465 'transmission' => 'automatic', 291 466 'helmets' => 2, 292 467 ), 293 'rates' => array(468 'rates' => array( 294 469 'hour' => 4, 295 470 'day' => 15, 296 471 'week' => 90, 297 472 ), 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' ) ), 301 476 ), 302 477 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( 307 484 'engine_cc' => 160, 308 485 'transmission' => 'automatic', 309 486 'helmets' => 2, 310 487 ), 311 'rates' => array(488 'rates' => array( 312 489 'day' => 22, 313 490 'week' => 130, 314 491 ), 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' ) ), 318 495 ), 319 496 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( 324 503 'seats' => 5, 325 504 'doors' => 5, … … 329 508 'luggage' => 2, 330 509 ), 331 'rates' => array(510 'rates' => array( 332 511 'day' => 45, 333 512 'week' => 270, 334 513 ), 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' ) ), 338 517 ), 339 518 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( 344 525 'seats' => 7, 345 526 'doors' => 5, … … 349 530 'luggage' => 3, 350 531 ), 351 'rates' => array(532 'rates' => array( 352 533 'day' => 65, 353 534 'week' => 390, 354 535 ), 355 'deposit' => 300,356 'units' => 1,357 'colors' => array( __( 'Black', 'rentbaik' ) ),536 'deposit' => 300, 537 'units' => 1, 538 'colors' => array( __( 'Black', 'rentbaik' ) ), 358 539 ), 359 540 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( 364 547 'frame_size' => 'M', 365 548 'gears' => 7, 366 549 ), 367 'rates' => array(550 'rates' => array( 368 551 'hour' => 3, 369 552 'day' => 10, 370 553 ), 371 'deposit' => 0,372 'units' => 2,373 'colors' => array( __( 'Green', 'rentbaik' ) ),554 'deposit' => 0, 555 'units' => 2, 556 'colors' => array( __( 'Green', 'rentbaik' ) ), 374 557 ), 375 558 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( 380 565 'frame_size' => 'M', 381 566 'range_km' => 60, 382 567 ), 383 'rates' => array(568 'rates' => array( 384 569 'hour' => 6, 385 570 'day' => 25, 386 571 ), 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>> 398 583 */ 399 584 private static function extra_definitions(): array { 400 585 return array( 401 array(586 'helmet' => array( 402 587 'name' => __( 'Extra helmet (sample)', 'rentbaik' ), 403 588 'description' => __( 'Clean helmet for a passenger.', 'rentbaik' ), … … 407 592 'vehicle_types' => array( 'motorcycle', 'bicycle', 'ebike' ), 408 593 ), 409 array(594 'poncho' => array( 410 595 'name' => __( 'Rain poncho (sample)', 'rentbaik' ), 411 596 'description' => __( 'Yours to keep.', 'rentbaik' ), … … 415 600 'vehicle_types' => array( 'motorcycle', 'bicycle', 'ebike' ), 416 601 ), 417 array(602 'child_seat' => array( 418 603 'name' => __( 'Child seat (sample)', 'rentbaik' ), 419 604 'description' => __( 'For children from 9 to 18 kg.', 'rentbaik' ), … … 490 675 491 676 /** 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. 493 781 * 494 782 * @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. 496 785 * @param int $location_id Location ID. 497 786 * @return array<string, int[]> 498 787 */ 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 ); 535 795 $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. 537 797 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'] ), 558 817 ); 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 } 559 827 $booking_id = is_int( $result ) ? $result : 0; 560 828 } … … 567 835 $data['customers'][] = (int) $booking['customer_id']; 568 836 } 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; 571 964 } 572 965 } … … 580 973 * @return int 581 974 */ 582 p rivatestatic function price( float $usd ): int {975 public static function price( float $usd ): int { 583 976 if ( $usd <= 0 ) { 584 977 return 0; … … 586 979 $currency = Format::currency(); 587 980 $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 ); 590 984 return (int) round( $amount * ( 10 ** Format::decimals() ) ); 591 985 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)