PromotionCouponService.php 2.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142
  1. <?php
  2. namespace App\Services;
  3. use Illuminate\Support\Facades\DB;
  4. use Illuminate\Support\Facades\Schema;
  5. /**
  6. * Backend side of F1 own-game coupons. Coupons are issued and redeemed by melbet_sport-api;
  7. * here refunds return the paid amount and give the coupon back.
  8. */
  9. class PromotionCouponService
  10. {
  11. /** What the member actually paid for an order: stake minus its coupon share. */
  12. public static function paid($stake, $discount): string
  13. {
  14. return bcsub(bcadd((string)$stake, '0', 2), bcadd((string)($discount ?? '0'), '0', 2), 2);
  15. }
  16. /** The coupon returns once every order sharing it is refunded; an expired coupon simply stays unusable. */
  17. public static function release(string $source, int $orderId): void
  18. {
  19. if (!Schema::hasTable('promotion_coupon_uses')) return;
  20. DB::transaction(function () use ($source, $orderId) {
  21. // This immutable link identifies the coupon; always lock the coupon before any use row.
  22. $link = DB::table('promotion_coupon_uses')->where('source', $source)->where('order_id', $orderId)->first();
  23. if (!$link) return;
  24. $coupon = DB::table('promotion_coupons')->where('id', $link->coupon_id)->lockForUpdate()->first();
  25. if (!$coupon) throw new \RuntimeException('退款优惠券记录不存在');
  26. $use = DB::table('promotion_coupon_uses')->where('id', $link->id)->whereNull('refunded_at')->lockForUpdate()->first();
  27. if (!$use) return;
  28. $now = PromotionFacts::now();
  29. DB::table('promotion_coupon_uses')->where('id', $use->id)->update(['refunded_at' => $now, 'updated_at' => $now]);
  30. // Current read: an outer REPEATABLE READ transaction may already have an older snapshot.
  31. $remaining = DB::table('promotion_coupon_uses')->where('coupon_id', $use->coupon_id)
  32. ->whereNull('refunded_at')->lockForUpdate()->get(['id']);
  33. if ($remaining->isNotEmpty()) return;
  34. DB::table('promotion_coupons')->where('id', $use->coupon_id)->where('status', 'used')
  35. ->update(['status' => 'unused', 'used_source' => null, 'used_stake' => null, 'used_at' => null, 'updated_at' => $now]);
  36. });
  37. }
  38. }