Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
StripeService
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 10
240
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createProduct
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 updateProduct
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 archiveProduct
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 createPrice
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
6
 archivePrice
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 syncPlanToStripe
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getProduct
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getPricesForProduct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 archiveAllPricesForProduct
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace App\Http\Services;
4
5use App\Http\Models\Plans;
6use Exception;
7use Illuminate\Support\Facades\Log;
8use Stripe\Exception\ApiErrorException;
9use Stripe\Price;
10use Stripe\Product;
11use 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 */
19class 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}