, 'qty'=>, 'unit_price'=>], ...] * Returns [amount, errorMessage|null]. */ function discount_evaluate(array $d, int $eventId, array $lines, string $email): array { $n = now(); if (!$d['is_active']) return [0, 'This code is not active.']; if ($d['event_id'] !== null && (int)$d['event_id'] !== $eventId) return [0, 'This code is not valid for this event.']; if ($d['starts_at'] !== null && $d['starts_at'] > $n) return [0, 'This code is not active yet.']; if ($d['ends_at'] !== null && $d['ends_at'] < $n) return [0, 'This code has expired.']; $eligQty = 0; $eligSub = 0; foreach ($lines as $l) { if ($d['category_id'] !== null && (int)$d['category_id'] !== (int)$l['category_id']) continue; $eligQty += $l['qty']; $eligSub += $l['qty'] * $l['unit_price']; } if ($eligQty === 0) return [0, 'This code does not apply to the selected tickets.']; if ($eligQty < (int)$d['min_qty']) return [0, 'This code needs at least ' . plural((int)$d['min_qty'], 'ticket') . '.']; if ($d['max_uses'] !== null && discount_uses((int)$d['id']) >= (int)$d['max_uses']) return [0, 'This code has been fully used.']; if ($d['max_per_email'] !== null && $email !== '' && discount_uses((int)$d['id'], $email) >= (int)$d['max_per_email']) { return [0, 'You have already used this code.']; } if ($d['type'] === 'percent') { $amount = intdiv($eligSub * min(100, (int)$d['value']), 100); } else { $amount = $d['applies_per'] === 'ticket' ? (int)$d['value'] * $eligQty : (int)$d['value']; } return [min($amount, $eligSub), null]; } /** * Pick the discount for an order: the entered code, if valid. * Returns ['discount' => row|null, 'amount' => int, 'code_error' => string|null]. * When $lock is true the chosen rows are locked FOR UPDATE (call inside a transaction). */ function discount_choose(int $eventId, array $lines, string $code, string $email, bool $lock = false): array { $best = ['discount' => null, 'amount' => 0, 'code_error' => null]; $suffix = $lock ? ' FOR UPDATE' : ''; $code = normalize_code($code); if ($code !== '') { $d = q1('SELECT * FROM {discounts} WHERE code = ?' . $suffix, [$code]); if (!$d) { $best['code_error'] = 'That code is not valid.'; } else { [$amt, $err] = discount_evaluate($d, $eventId, $lines, $email); if ($err) $best['code_error'] = $err; else $best = ['discount' => $d, 'amount' => $amt, 'code_error' => null]; } } // Add-ons can offer other discounts (for example automatic ones) and keep whichever saves the buyer more. return (array)apply_filters('gatecraft_discount_choice', $best, $eventId, $lines, $email, $lock); } function discount_describe(array $d): string { $v = $d['type'] === 'percent' ? (int)$d['value'] . '% off' : money((int)$d['value']) . ' off' . ($d['applies_per'] === 'ticket' ? ' per ticket' : ''); return $v . ($d['min_qty'] > 1 ? ', min ' . (int)$d['min_qty'] . ' tickets' : ''); }