first(); return $this->evaluate($coupon, $customer, $product, $billingCycle, $subTotal, $context); } /** * @return array{coupon: Coupon, discount: int} */ public function lockAndApply(string $code, Customer $customer, Product $product, string $billingCycle, int $subTotal, string $context = Coupon::APPLIES_NEW): array { $coupon = Coupon::where('code', Coupon::normalizeCode($code))->lockForUpdate()->first(); return $this->evaluate($coupon, $customer, $product, $billingCycle, $subTotal, $context); } public function record(Coupon $coupon, Order $order, int $discount): CouponRedemption { return CouponRedemption::create([ 'coupon_id' => $coupon->id, 'customer_id' => $order->customer_id, 'order_id' => $order->id, 'amount' => $discount, ]); } public function setReleased(Order $order, bool $released): void { CouponRedemption::where('order_id', $order->id) ->update(['released_at' => $released ? now() : null]); } /** * @return array{coupon: Coupon, discount: int} */ private function evaluate(?Coupon $coupon, Customer $customer, Product $product, string $billingCycle, int $subTotal, string $context): array { if (! $coupon || ! $coupon->is_active) { $this->fail('Mã giảm giá không hợp lệ.'); } if ($coupon->starts_at?->isFuture()) { $this->fail('Mã giảm giá chưa đến thời gian áp dụng.'); } if ($coupon->ends_at?->isPast()) { $this->fail('Mã giảm giá đã hết hạn.'); } if (! in_array($coupon->applies_to, [$context, Coupon::APPLIES_ALL], true)) { $this->fail($context === Coupon::APPLIES_NEW ? 'Mã giảm giá này không áp dụng cho đơn mới.' : 'Mã giảm giá này chỉ áp dụng cho đơn mới.'); } if (! $this->inScope($coupon, $product, $billingCycle)) { $this->fail('Mã giảm giá không áp dụng cho sản phẩm hoặc chu kỳ thanh toán này.'); } if ($subTotal < $coupon->min_order) { $this->fail(sprintf('Đơn hàng tối thiểu %s VNĐ để áp dụng mã này.', number_format($coupon->min_order))); } $this->assertUsageLeft($coupon, $customer); $discount = $this->amountFor($coupon, $subTotal); if ($discount <= 0) { $this->fail('Đơn hàng không đủ điều kiện áp dụng mã giảm giá.'); } return ['coupon' => $coupon, 'discount' => $discount]; } private function inScope(Coupon $coupon, Product $product, string $billingCycle): bool { return $this->matches($coupon->product_types, $product->product_type) && $this->matches($coupon->product_ids, $product->id) && $this->matches($coupon->billing_cycles, $billingCycle); } private function matches(?array $allowed, string|int|null $value): bool { return empty($allowed) || in_array($value, $allowed, false); } private function assertUsageLeft(Coupon $coupon, Customer $customer): void { $redemptions = $coupon->activeRedemptions(); if ($coupon->usage_limit !== null && (clone $redemptions)->count() >= $coupon->usage_limit) { $this->fail('Mã giảm giá đã hết lượt sử dụng.'); } if ($coupon->per_customer_limit !== null && (clone $redemptions)->where('customer_id', $customer->id)->count() >= $coupon->per_customer_limit) { $this->fail('Bạn đã dùng hết số lần cho phép của mã giảm giá này.'); } } private function amountFor(Coupon $coupon, int $subTotal): int { $discount = $coupon->type === Coupon::TYPE_PERCENT ? intdiv($subTotal * $coupon->value, 100) : $coupon->value; if ($coupon->max_discount !== null) { $discount = min($discount, $coupon->max_discount); } return min($discount, $subTotal - self::MIN_PAYABLE); } private function fail(string $message): never { throw ValidationException::withMessages(['coupon_code' => $message]); } }