substr( sanitize_key( $type ), 0, 20 ), 'object_id' => $id, 'action' => substr( sanitize_key( $action ), 0, 32 ), 'message' => $message, 'user_id' => get_current_user_id(), 'created_at' => self::now(), ), array( '%s', '%d', '%s', '%s', '%d', '%s' ) ); } /** * Entries of one object, newest first. * * @param string $type Object type. * @param int $id Object ID. * @param int $limit Maximum entries. * @return array> */ public static function for_object( string $type, int $id, int $limit = 50 ): array { $table = self::table(); return (array) self::remember( 'object:' . $type . ':' . $id . ':' . $limit, static function () use ( $table, $type, $id, $limit ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; cached by remember(). return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE object_type = %s AND object_id = %d ORDER BY id DESC LIMIT %d', $table, $type, $id, $limit ), ARRAY_A ); } ); } /** * Delete the entries of one object (when the object itself is deleted). * * @param string $type Object type. * @param int $id Object ID. * @return void */ public static function delete_for( string $type, int $id ): void { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table write; cache invalidated by touch(). $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE object_type = %s AND object_id = %d', self::table(), $type, $id ) ); self::touch(); } /** * Name of who made a change. * * @param array $entry Log row. * @return string */ public static function who( array $entry ): string { $user_id = (int) $entry['user_id']; if ( $user_id <= 0 ) { return __( 'Website or system', 'rentbaik' ); } $user = get_userdata( $user_id ); return $user ? $user->display_name : __( 'Deleted user', 'rentbaik' ); } /** * Print a list of entries. * * @param array> $entries Log rows. * @param string $none_text Text when there are none. * @return void */ public static function render_list( array $entries, string $none_text ): void { if ( empty( $entries ) ) { if ( '' !== $none_text ) { echo '

' . esc_html( $none_text ) . '

'; } return; } echo '
    '; foreach ( $entries as $entry ) { echo '
  1. ' . esc_html( Format::datetime( (string) $entry['created_at'] ) ) . ''; echo '' . esc_html( (string) $entry['message'] ) . ''; echo '' . esc_html( self::who( $entry ) ) . '
  2. '; } echo '
'; } /** * Booking created. * * @param int $booking_id Booking ID. * @param array $input Create input. * @return void */ public static function on_created( int $booking_id, array $input ): void { $source = BookingService::source_labels()[ $input['source'] ?? 'admin' ] ?? (string) ( $input['source'] ?? '' ); $item = BookingRepository::vehicle_item( $booking_id ); $unit = $item ? UnitRepository::find( (int) $item['unit_id'] ) : null; $message = sprintf( /* translators: 1: source, for example "Website", 2: plate number or "no unit". */ __( 'Booking created via %1$s, unit %2$s.', 'rentbaik' ), $source, $unit ? $unit['plate_number'] : __( 'not assigned yet', 'rentbaik' ) ); self::add( 'booking', $booking_id, 'created', $message ); } /** * Booking status changed. * * @param int $booking_id Booking ID. * @param string $status New status. * @param string $old Old status. * @return void */ public static function on_status( int $booking_id, string $status, string $old ): void { $labels = BookingService::status_labels(); /* translators: 1: old status, 2: new status. */ self::add( 'booking', $booking_id, 'status', sprintf( __( 'Status: %1$s → %2$s.', 'rentbaik' ), $labels[ $old ] ?? $old, $labels[ $status ] ?? $status ) ); } /** * Booking dates or unit changed. * * @param int $booking_id Booking ID. * @param array $result Reschedule result. * @return void */ public static function on_rescheduled( int $booking_id, array $result ): void { $message = sprintf( /* translators: 1: old pick-up, 2: old return, 3: new pick-up, 4: new return. */ __( 'Dates: %1$s – %2$s → %3$s – %4$s.', 'rentbaik' ), Format::datetime( (string) $result['old_pickup'] ), Format::datetime( (string) $result['old_return'] ), Format::datetime( (string) $result['pickup'] ), Format::datetime( (string) $result['return'] ) ); if ( (int) $result['unit'] !== (int) $result['old_unit'] ) { $old = UnitRepository::find( (int) $result['old_unit'] ); $new = UnitRepository::find( (int) $result['unit'] ); $from = $old ? $old['plate_number'] : __( 'no unit', 'rentbaik' ); /* translators: 1: old plate number, 2: new plate number. */ $message .= ' ' . sprintf( __( 'Unit: %1$s → %2$s.', 'rentbaik' ), $from, $new ? $new['plate_number'] : '?' ); } if ( (int) $result['total'] !== (int) $result['old_total'] ) { /* translators: 1: old price, 2: new price. */ $message .= ' ' . sprintf( __( 'Price: %1$s → %2$s', 'rentbaik' ), Format::money( (int) $result['old_total'], (string) $result['currency'] ), Format::money( (int) $result['total'], (string) $result['currency'] ) ) . '.'; } self::add( 'booking', $booking_id, 'moved', $message ); } /** * Payment recorded by staff. * * @param int $booking_id Booking ID. * @param array $payment Payment data. * @return void */ public static function on_payment( int $booking_id, array $payment ): void { $booking = BookingRepository::find( $booking_id ); $method = PaymentRepository::method_labels()[ $payment['method'] ?? '' ] ?? (string) ( $payment['method'] ?? '' ); $type = (string) ( $payment['payment_type'] ?? 'payment' ); $amount = Format::money( (int) ( $payment['amount'] ?? 0 ), $booking ? (string) $booking['currency'] : '' ); $texts = array( /* translators: 1: amount, 2: payment method. */ 'payment' => __( 'Payment recorded: %1$s (%2$s).', 'rentbaik' ), /* translators: 1: amount, 2: payment method. */ 'refund' => __( 'Refund recorded: %1$s (%2$s).', 'rentbaik' ), /* translators: 1: amount, 2: payment method. */ 'deposit' => __( 'Deposit received: %1$s (%2$s).', 'rentbaik' ), /* translators: 1: amount, 2: payment method. */ 'deposit_refund' => __( 'Deposit returned: %1$s (%2$s).', 'rentbaik' ), ); self::add( 'booking', $booking_id, 'payment', sprintf( $texts[ $type ] ?? $texts['payment'], $amount, $method ) ); } /** * Online payment received (WooCommerce). * * @param int $booking_id Booking ID. * @param int $amount Amount in minor units. * @param string $method Method key. * @return void */ public static function on_online_payment( int $booking_id, int $amount, string $method ): void { $booking = BookingRepository::find( $booking_id ); $label = PaymentRepository::method_labels()[ $method ] ?? $method; /* translators: 1: amount, 2: payment method. */ self::add( 'booking', $booking_id, 'online_payment', sprintf( __( 'Online payment received: %1$s (%2$s).', 'rentbaik' ), Format::money( $amount, $booking ? (string) $booking['currency'] : '' ), $label ) ); } /** * Settings saved: record which fields changed, never their values (keys and secrets stay out of the log). * * @param string $group Settings group. * @param mixed $before Old value. * @param mixed $after New value. * @return void */ public static function on_settings( string $group, $before, $after ): void { $before = is_array( $before ) ? $before : array(); $after = is_array( $after ) ? $after : array(); $changed = array(); foreach ( array_unique( array_merge( array_keys( $before ), array_keys( $after ) ) ) as $key ) { if ( ( $before[ $key ] ?? null ) !== ( $after[ $key ] ?? null ) ) { $changed[] = str_replace( '_', ' ', (string) $key ); } } if ( empty( $changed ) ) { return; } /* translators: 1: settings group, 2: list of changed fields. */ self::add( 'settings', 0, $group, sprintf( __( 'Settings “%1$s” changed: %2$s.', 'rentbaik' ), $group, implode( ', ', $changed ) ) ); } }