Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 96 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
| StripeService | |
0.00% |
0 / 96 |
|
0.00% |
0 / 10 |
240 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| createProduct | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
| updateProduct | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
6 | |||
| archiveProduct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
| createPrice | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
6 | |||
| archivePrice | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
| syncPlanToStripe | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
| getProduct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
| getPricesForProduct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
| archiveAllPricesForProduct | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Http\Services; |
| 4 | |
| 5 | use App\Http\Models\Plans; |
| 6 | use Exception; |
| 7 | use Illuminate\Support\Facades\Log; |
| 8 | use Stripe\Exception\ApiErrorException; |
| 9 | use Stripe\Price; |
| 10 | use Stripe\Product; |
| 11 | use Stripe\StripeClient; |
| 12 | |
| 13 | /** |
| 14 | * Service for Stripe product and price synchronization. |
| 15 | * |
| 16 | * Handles creating, updating, and archiving Stripe products and prices |
| 17 | * to keep them in sync with plan data in the database. |
| 18 | */ |
| 19 | class StripeService |
| 20 | { |
| 21 | /** |
| 22 | * The Stripe client instance. |
| 23 | */ |
| 24 | private StripeClient $stripe; |
| 25 | |
| 26 | /** |
| 27 | * Create a new StripeService instance. |
| 28 | */ |
| 29 | public function __construct() |
| 30 | { |
| 31 | $this->stripe = new StripeClient(config('services.stripe.secret')); |
| 32 | } |
| 33 | |
| 34 | /** |
| 35 | * Create a Stripe product for a plan. |
| 36 | * |
| 37 | * @param Plans $plan The plan to create a product for |
| 38 | * @return Product The created Stripe product |
| 39 | * |
| 40 | * @throws ApiErrorException If Stripe API call fails |
| 41 | */ |
| 42 | public function createProduct(Plans $plan): Product |
| 43 | { |
| 44 | $product = $this->stripe->products->create([ |
| 45 | 'name' => $plan->title, |
| 46 | 'metadata' => [ |
| 47 | 'plan_id' => (string) $plan->_id, |
| 48 | 'plan_identifier' => $plan->identifier, |
| 49 | ], |
| 50 | ]); |
| 51 | |
| 52 | Log::info('Stripe product created', [ |
| 53 | 'plan_id' => (string) $plan->_id, |
| 54 | 'stripe_product_id' => $product->id, |
| 55 | ]); |
| 56 | |
| 57 | return $product; |
| 58 | } |
| 59 | |
| 60 | /** |
| 61 | * Update a Stripe product for a plan. |
| 62 | * |
| 63 | * @param Plans $plan The plan with updated data |
| 64 | * @return Product|null The updated Stripe product or null if no product exists |
| 65 | * |
| 66 | * @throws ApiErrorException If Stripe API call fails |
| 67 | */ |
| 68 | public function updateProduct(Plans $plan): ?Product |
| 69 | { |
| 70 | if (! $plan->stripe_product_id) { |
| 71 | Log::warning('Cannot update Stripe product - no stripe_product_id', [ |
| 72 | 'plan_id' => (string) $plan->_id, |
| 73 | ]); |
| 74 | |
| 75 | return null; |
| 76 | } |
| 77 | |
| 78 | $product = $this->stripe->products->update($plan->stripe_product_id, [ |
| 79 | 'name' => $plan->title, |
| 80 | 'metadata' => [ |
| 81 | 'plan_id' => (string) $plan->_id, |
| 82 | 'plan_identifier' => $plan->identifier, |
| 83 | ], |
| 84 | ]); |
| 85 | |
| 86 | Log::info('Stripe product updated', [ |
| 87 | 'plan_id' => (string) $plan->_id, |
| 88 | 'stripe_product_id' => $product->id, |
| 89 | ]); |
| 90 | |
| 91 | return $product; |
| 92 | } |
| 93 | |
| 94 | /** |
| 95 | * Archive a Stripe product. |
| 96 | * |
| 97 | * @param string $productId The Stripe product ID |
| 98 | * @return Product The archived Stripe product |
| 99 | * |
| 100 | * @throws ApiErrorException If Stripe API call fails |
| 101 | */ |
| 102 | public function archiveProduct(string $productId): Product |
| 103 | { |
| 104 | $product = $this->stripe->products->update($productId, [ |
| 105 | 'active' => false, |
| 106 | ]); |
| 107 | |
| 108 | Log::info('Stripe product archived', [ |
| 109 | 'stripe_product_id' => $productId, |
| 110 | ]); |
| 111 | |
| 112 | return $product; |
| 113 | } |
| 114 | |
| 115 | /** |
| 116 | * Create a Stripe price for a plan. |
| 117 | * |
| 118 | * @param Plans $plan The plan to create a price for |
| 119 | * @param string $interval The billing interval (month, year) |
| 120 | * @param int $amount The price in cents |
| 121 | * @param string $currency The currency code (default: usd) |
| 122 | * @return Price The created Stripe price |
| 123 | * |
| 124 | * @throws ApiErrorException If Stripe API call fails |
| 125 | * @throws Exception If plan has no Stripe product ID |
| 126 | */ |
| 127 | public function createPrice(Plans $plan, string $interval, int $amount, string $currency = 'usd'): Price |
| 128 | { |
| 129 | if (! $plan->stripe_product_id) { |
| 130 | throw new Exception('Plan must have a stripe_product_id to create a price'); |
| 131 | } |
| 132 | |
| 133 | $price = $this->stripe->prices->create([ |
| 134 | 'product' => $plan->stripe_product_id, |
| 135 | 'unit_amount' => $amount, |
| 136 | 'currency' => $currency, |
| 137 | 'recurring' => [ |
| 138 | 'interval' => $interval, |
| 139 | ], |
| 140 | 'metadata' => [ |
| 141 | 'plan_id' => (string) $plan->_id, |
| 142 | 'plan_identifier' => $plan->identifier, |
| 143 | ], |
| 144 | ]); |
| 145 | |
| 146 | Log::info('Stripe price created', [ |
| 147 | 'plan_id' => (string) $plan->_id, |
| 148 | 'stripe_price_id' => $price->id, |
| 149 | 'interval' => $interval, |
| 150 | 'amount' => $amount, |
| 151 | ]); |
| 152 | |
| 153 | return $price; |
| 154 | } |
| 155 | |
| 156 | /** |
| 157 | * Archive a Stripe price. |
| 158 | * |
| 159 | * @param string $priceId The Stripe price ID |
| 160 | * @return Price The archived Stripe price |
| 161 | * |
| 162 | * @throws ApiErrorException If Stripe API call fails |
| 163 | */ |
| 164 | public function archivePrice(string $priceId): Price |
| 165 | { |
| 166 | $price = $this->stripe->prices->update($priceId, [ |
| 167 | 'active' => false, |
| 168 | ]); |
| 169 | |
| 170 | Log::info('Stripe price archived', [ |
| 171 | 'stripe_price_id' => $priceId, |
| 172 | ]); |
| 173 | |
| 174 | return $price; |
| 175 | } |
| 176 | |
| 177 | /** |
| 178 | * Sync a plan to Stripe (create or update product). |
| 179 | * |
| 180 | * This method creates a new Stripe product if the plan doesn't have one, |
| 181 | * or updates the existing product if it does. |
| 182 | * |
| 183 | * @param Plans $plan The plan to sync |
| 184 | * @return array{product: Product, created: bool} The product and whether it was created |
| 185 | * |
| 186 | * @throws ApiErrorException If Stripe API call fails |
| 187 | */ |
| 188 | public function syncPlanToStripe(Plans $plan): array |
| 189 | { |
| 190 | $created = false; |
| 191 | |
| 192 | if ($plan->stripe_product_id) { |
| 193 | // Update existing product |
| 194 | $product = $this->updateProduct($plan); |
| 195 | } else { |
| 196 | // Create new product |
| 197 | $product = $this->createProduct($plan); |
| 198 | $created = true; |
| 199 | } |
| 200 | |
| 201 | return [ |
| 202 | 'product' => $product, |
| 203 | 'created' => $created, |
| 204 | ]; |
| 205 | } |
| 206 | |
| 207 | /** |
| 208 | * Get a Stripe product by ID. |
| 209 | * |
| 210 | * @param string $productId The Stripe product ID |
| 211 | * @return Product|null The Stripe product or null if not found |
| 212 | */ |
| 213 | public function getProduct(string $productId): ?Product |
| 214 | { |
| 215 | try { |
| 216 | return $this->stripe->products->retrieve($productId); |
| 217 | } catch (ApiErrorException $e) { |
| 218 | Log::warning('Failed to retrieve Stripe product', [ |
| 219 | 'stripe_product_id' => $productId, |
| 220 | 'error' => $e->getMessage(), |
| 221 | ]); |
| 222 | |
| 223 | return null; |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | /** |
| 228 | * Get all prices for a Stripe product. |
| 229 | * |
| 230 | * @param string $productId The Stripe product ID |
| 231 | * @return array<Price> Array of Stripe prices |
| 232 | * |
| 233 | * @throws ApiErrorException If Stripe API call fails |
| 234 | */ |
| 235 | public function getPricesForProduct(string $productId): array |
| 236 | { |
| 237 | $prices = $this->stripe->prices->all([ |
| 238 | 'product' => $productId, |
| 239 | 'active' => true, |
| 240 | ]); |
| 241 | |
| 242 | return $prices->data; |
| 243 | } |
| 244 | |
| 245 | /** |
| 246 | * Archive all prices for a Stripe product. |
| 247 | * |
| 248 | * @param string $productId The Stripe product ID |
| 249 | * @return int Number of prices archived |
| 250 | * |
| 251 | * @throws ApiErrorException If Stripe API call fails |
| 252 | */ |
| 253 | public function archiveAllPricesForProduct(string $productId): int |
| 254 | { |
| 255 | $prices = $this->getPricesForProduct($productId); |
| 256 | $archivedCount = 0; |
| 257 | |
| 258 | foreach ($prices as $price) { |
| 259 | $this->archivePrice($price->id); |
| 260 | $archivedCount++; |
| 261 | } |
| 262 | |
| 263 | Log::info('Archived all prices for Stripe product', [ |
| 264 | 'stripe_product_id' => $productId, |
| 265 | 'archived_count' => $archivedCount, |
| 266 | ]); |
| 267 | |
| 268 | return $archivedCount; |
| 269 | } |
| 270 | } |