insert( Plugin::table( 'orders' ), array( 'customer_id' => (int) $customer_id, 'kind' => 'booking', 'gateway' => 'on_site', 'currency' => Settings::get( 'currency', 'USD' ), 'token' => Repository::token(), 'created_at' => current_time( 'mysql', true ), ) ); return (int) $wpdb->insert_id; } public static function get( $id ) { return Repository::find( 'orders', $id ); } public static function update( $id, array $d ) { global $wpdb; $wpdb->update( Plugin::table( 'orders' ), $d, array( 'id' => (int) $id ) ); } public static function appointments( $order_id ) { global $wpdb; $t = Plugin::table( 'appointments' ); $ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM {$t} WHERE order_id = %d ORDER BY start_utc", $order_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared return array_values( array_filter( array_map( array( 'Bookzenix\\Repository', 'appointment' ), $ids ) ) ); } /** One line per appointment. @return array[] { type, id, label, qty, price, total, status } */ public static function lines( array $order ) { $lines = array(); foreach ( self::appointments( $order['id'] ) as $a ) { $lines[] = array( 'type' => 'appointment', 'id' => (int) $a['id'], 'label' => sprintf( '%s – %s %s', $a['service_name'], $a['date_local'], $a['time_local'] ), 'qty' => 1, 'price' => (float) $a['price'], 'total' => (float) $a['price'], 'status' => $a['status'], ); } return $lines; } /** Recompute the order total (canceled and rejected appointments excluded). */ public static function recalc( $order_id ) { $o = self::get( $order_id ); if ( ! $o ) { return null; } $total = 0; foreach ( self::lines( $o ) as $l ) { if ( ! in_array( $l['status'], array( 'canceled', 'rejected' ), true ) ) { $total += $l['total']; } } $data = array( 'subtotal' => round( $total, 2 ), 'total' => round( $total, 2 ), ); self::update( $order_id, $data ); return array_merge( $o, $data ); } /** Money received at the visit, entered by staff. */ public static function record_payment( $order_id, $gateway, $amount, $ref = '' ) { global $wpdb; $o = self::get( $order_id ); if ( ! $o || $amount <= 0 ) { return false; } $wpdb->insert( Plugin::table( 'payments' ), array( 'order_id' => $order_id, 'kind' => 'payment', 'gateway' => 'on_site', 'gateway_ref' => $ref ?: null, 'amount' => round( $amount, 2 ), 'currency' => $o['currency'], 'status' => 'paid', 'created_at' => current_time( 'mysql', true ), ) ); self::apply_totals( $order_id ); do_action( 'bookzenix_payment_recorded', $order_id, 'on_site', $amount ); return true; } /** Recompute what was paid and spread it over the order's appointments. */ public static function apply_totals( $order_id ) { global $wpdb; $p = Plugin::table( 'payments' ); $paid = round( (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(amount),0) FROM {$p} WHERE order_id = %d AND kind = 'payment' AND status = 'paid'", $order_id ) ), 2 ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $o = self::get( $order_id ); if ( ! $o ) { return; } $status = $paid <= 0 ? 'unpaid' : ( $paid + 0.001 >= (float) $o['total'] ? 'paid' : 'partial' ); self::update( $order_id, array( 'paid' => $paid, 'payment_status' => $status ) ); $lines = array_values( array_filter( self::lines( $o ), function ( $l ) { return ! in_array( $l['status'], array( 'canceled', 'rejected' ), true ); } ) ); $sum = array_sum( array_column( $lines, 'total' ) ); $left = $paid; $n = count( $lines ); foreach ( $lines as $i => $l ) { $share = $sum > 0 ? ( $i === $n - 1 ? $left : round( $paid * $l['total'] / $sum, 2 ) ) : 0; $share = max( 0, min( $share, $left ) ); $left -= $share; $st = $share <= 0 ? 'unpaid' : ( $share + 0.001 >= $l['total'] ? 'paid' : 'partial' ); $wpdb->update( Plugin::table( 'appointments' ), array( 'paid' => round( $share, 2 ), 'payment_status' => $st ), array( 'id' => $l['id'] ) ); } } }