prefix . 'seatt_promos'; } public static function round_money( $v ) { $code = strtoupper( (string) get_option( 'seatt_currency_code', 'IDR' ) ); $zero = function_exists( 'seatt_zero_decimal_currencies' ) ? seatt_zero_decimal_currencies() : array( 'IDR', 'JPY' ); return in_array( $code, (array) $zero, true ) ? round( $v ) : round( $v, 2 ); } /* ---------------- early bird ---------------- */ /** Active early-bird for an event, or null. */ public static function early_bird( $event_id ) { $until = (string) get_post_meta( $event_id, '_seatt_earlybird_until', true ); $value = (float) get_post_meta( $event_id, '_seatt_earlybird_value', true ); if ( '' === $until || $value <= 0 ) { return null; } $ends = strtotime( strlen( $until ) <= 10 ? $until . ' 23:59:59' : $until ); if ( ! $ends || current_time( 'timestamp' ) > $ends ) { return null; } return array( 'type' => 'amount' === get_post_meta( $event_id, '_seatt_earlybird_type', true ) ? 'amount' : 'percent', 'value' => $value, 'until' => $until, ); } /* ---------------- promo codes ---------------- */ public static function normalize_code( $code ) { return strtoupper( preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $code ) ); } public static function get_promo( $code ) { global $wpdb; $code = self::normalize_code( $code ); if ( '' === $code ) { return null; } return $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE code = %s', $wpdb->seatt_promos, $code ) ); } /** Paid + awaiting-payment bookings that used the code. Abandoned * (cancelled) checkouts give their use back. */ public static function promo_uses( $code, $email = '' ) { global $wpdb; $code = self::normalize_code( $code ); if ( '' !== $email ) { return (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE promo_code = %s AND status IN ( \'paid\', \'pending_payment\' ) AND buyer_email = %s', $wpdb->seatt_bookings, $code, $email ) ); } return (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE promo_code = %s AND status IN ( \'paid\', \'pending_payment\' )', $wpdb->seatt_bookings, $code ) ); } /** * Returns the promo row if $code may be used for this order, or a * WP_Error explaining why not (shown to the buyer as-is). */ public static function validate_promo( $code, $event_id, array $lines, $email = '' ) { $p = self::get_promo( $code ); if ( ! $p || ! (int) $p->active ) { return new WP_Error( 'seatt_promo_invalid', __( 'That promo code is not valid.', 'seathive' ) ); } $now = current_time( 'timestamp' ); if ( $p->starts_at && strtotime( $p->starts_at ) > $now ) { return new WP_Error( 'seatt_promo_early', __( 'That promo code is not active yet.', 'seathive' ) ); } if ( $p->ends_at && strtotime( $p->ends_at ) < $now ) { return new WP_Error( 'seatt_promo_expired', __( 'That promo code has expired.', 'seathive' ) ); } if ( (int) $p->event_id && (int) $p->event_id !== (int) $event_id ) { return new WP_Error( 'seatt_promo_event', __( 'That promo code is not valid for this event.', 'seathive' ) ); } $qty = 0; $eligible = 0; foreach ( $lines as $l ) { $qty += (int) $l['qty']; if ( '' === (string) $p->category || strtolower( $l['category'] ) === strtolower( $p->category ) ) { $eligible += (int) $l['qty']; } } if ( ! $eligible ) { return new WP_Error( 'seatt_promo_category', sprintf( /* translators: %s: value */ __( 'That promo code only applies to %s tickets.', 'seathive' ), class_exists( 'SEATT_Categories' ) ? SEATT_Categories::name( $event_id, $p->category ) : ucfirst( $p->category ) ) ); } if ( (int) $p->min_tickets > 0 && $qty < (int) $p->min_tickets ) { return new WP_Error( 'seatt_promo_min', sprintf( /* translators: %d: number */ __( 'That promo code needs at least %d tickets in the order.', 'seathive' ), (int) $p->min_tickets ) ); } if ( (int) $p->max_uses > 0 && self::promo_uses( $p->code ) >= (int) $p->max_uses ) { return new WP_Error( 'seatt_promo_used_up', __( 'That promo code has been fully used.', 'seathive' ) ); } if ( (int) $p->per_email > 0 && '' !== $email && self::promo_uses( $p->code, $email ) >= (int) $p->per_email ) { return new WP_Error( 'seatt_promo_per_email', __( 'You have already used this promo code.', 'seathive' ) ); } return $p; } /* ---------------- the quote ---------------- */ /** * $lines: list of array( 'category' => 'vip', 'price' => 50000, 'qty' => 1 ). * Returns array with subtotal, early_bird, discount, fee, tax, total, * promo_code, plus 'promo_error' when a code was given but refused. */ public static function quote( $event_id, array $lines, $promo_code = '', $email = '' ) { $out = array( 'subtotal' => 0.0, 'early_bird' => 0.0, 'discount' => 0.0, 'fee' => 0.0, 'tax' => 0.0, 'total' => 0.0, 'promo_code' => '', 'promo_error' => '', 'fee_label' => (string) get_option( 'seatt_fee_label', '' ) ?: __( 'Service fee', 'seathive' ), ); $is_free = 'free' === ( get_post_meta( $event_id, '_seatt_booking_type', true ) ?: 'paid' ); if ( $is_free ) { return $out; } $qty = 0; foreach ( $lines as $l ) { $out['subtotal'] += (float) $l['price'] * (int) $l['qty']; $qty += (int) $l['qty']; } $running = $out['subtotal']; // Early bird (automatic, whole order). if ( $eb = self::early_bird( $event_id ) ) { $off = 'percent' === $eb['type'] ? $running * min( 100, $eb['value'] ) / 100 : min( $running, $eb['value'] * $qty ); $out['early_bird'] = self::round_money( $off ); $running -= $out['early_bird']; } // Promo code (optionally only on one category's tickets). if ( '' !== trim( (string) $promo_code ) ) { $p = self::validate_promo( $promo_code, $event_id, $lines, $email ); if ( is_wp_error( $p ) ) { $out['promo_error'] = $p->get_error_message(); } else { $base = 0.0; $eqty = 0; foreach ( $lines as $l ) { if ( '' === (string) $p->category || strtolower( $l['category'] ) === strtolower( $p->category ) ) { $base += (float) $l['price'] * (int) $l['qty']; $eqty += (int) $l['qty']; } } // Early bird already shrank the order proportionally. if ( $out['subtotal'] > 0 ) { $base *= $running / $out['subtotal']; } $off = 'percent' === $p->type ? $base * min( 100, (float) $p->value ) / 100 : min( $base, (float) $p->value ); $out['discount'] = self::round_money( min( $running, $off ) ); $out['promo_code'] = $p->code; $running -= $out['discount']; } } $running = max( 0, $running ); // Service fee — charged per ticket or as % of the discounted price. $fee_type = (string) get_option( 'seatt_fee_type', 'none' ); $fee_val = (float) get_option( 'seatt_fee_value', 0 ); if ( $fee_val > 0 && $running > 0 ) { if ( 'percent' === $fee_type ) { $out['fee'] = self::round_money( $running * $fee_val / 100 ); } elseif ( 'per_ticket' === $fee_type ) { $out['fee'] = self::round_money( $fee_val * $qty ); } } $running += $out['fee']; // Tax — same Store Setting the plugin always used. if ( get_option( 'seatt_show_tax_in_cart' ) ) { $rate = (float) get_option( 'seatt_tax_rate', 0 ); if ( $rate > 0 ) { $out['tax'] = self::round_money( $running * $rate / 100 ); } } $out['total'] = self::round_money( $running + $out['tax'] ); return $out; } }